Flask框架学习手册全绝不原创的飞龙

欢迎阅读《学习Flask》,这本书将教会您使用Flask构建Web应用程序所需的必要技能,这是一个轻量级的PythonWeb框架。本书采用了一个以示例驱动的方法,旨在让您快速入门。实际示例与适量的背景信息相结合,以确保您不仅了解Flask开发的如何,还了解为什么。

Flask最初是由ArminRonacher在2010年作为复杂的愚人节恶作剧的一部分发布的。该项目吹嘘自己是“下一代Python微型Web框架”,并讽刺了类似微框架所流行的功能。尽管Flask原本是一个恶作剧,但作者们对该项目引起了许多人的严肃兴趣感到意外。

Flask是一个建立在两个优秀库之上的微框架:Jinja2模板引擎和WerkzeugWSGI工具包。尽管与其他框架(如Django和Pylons)相比,Flask是一个相对较新的框架,但它已经获得了大量忠实的追随者。Flask为常见的Web开发任务提供了强大的工具,并鼓励采用自己的库来处理其他一切,使程序员有灵活性来选择最佳组件来构建他们的应用程序。每个Flask应用程序都是不同的,正如项目的文档所述,“Flask很有趣”。

Flask微框架在设计和API方面代表了与大多数其他流行的PythonWeb框架的不同,这导致许多新手开发人员问:“构建应用程序的正确方法是什么?”Flask对于我们开发者应该如何构建应用程序并没有提供强烈的意见。相反,它提供了关于构建应用程序所需的意见。Flask可以被认为是一组对象和函数,用于处理常见的Web任务,如将URL路由到代码、处理请求数据和渲染模板。虽然Flask提供的灵活性令人振奋,但它也可能导致混乱和糟糕的设计。

本书的目的是帮助您将这种灵活性视为机会。在本书的过程中,我们将构建并逐步增强一个由Flask驱动的博客网站。通过向网站添加新功能来介绍新概念。到本书结束时,我们将创建一个功能齐全的网站,您将对Flask及其常用扩展和库生态系统有着扎实的工作知识。

《第一章》《创建您的第一个Flask应用程序》以大胆宣言“Flask很有趣”开始,这是当您查看官方Flask文档时看到的第一件事情之一,在本章中,您将了解为什么许多Python开发人员都同意这一观点。

《第二章》《使用SQLAlchemy的关系数据库》指出,关系数据库是几乎所有现代Web应用程序构建的基石。我们将使用SQLAlchemy,这是一个强大的对象关系映射器,可以让我们抽象出多个数据库引擎的复杂性。在本章中,您将了解您早期选择的数据模型将影响随后代码的几乎每个方面。

《第三章》《模板和视图》涵盖了框架中最具代表性的两个组件:Jinja2模板语言和URL路由框架。我们将完全沉浸在Flask中,看到我们的应用程序最终开始成形。随着我们在本章的进展,我们的应用程序将开始看起来像一个真正的网站。

第四章表单和验证,向您展示如何使用表单直接通过由流行的WTForms库处理的网站修改博客内容。这是一个有趣的章节,因为我们将添加各种与网站交互的新方式。我们将创建与我们的数据模型一起工作的表单,并学习如何接收和验证用户数据。

第五章用户认证,解释了如何向您的网站添加用户认证。能够区分一个用户和另一个用户使我们能够开发一整套新的功能。例如,我们将看到如何限制对创建、编辑和删除视图的访问,防止匿名用户篡改网站内容。我们还可以向用户显示他们的草稿帖子,但对其他人隐藏。

第六章建立管理仪表板,向您展示如何为您的网站构建一个管理仪表板,使用优秀的Flask-Admin。我们的管理仪表板将使特定选定的用户能够管理整个网站上的所有内容。实质上,管理站点将是数据库的图形前端,支持创建、编辑和删除应用程序表中的行的操作。

第七章AJAX和RESTfulAPI,使用Flask-Restless为博客应用程序创建RESTfulAPI。RESTfulAPI是一种强大的访问应用程序的方式,通过提供高度结构化的数据来表示它。Flask-Restless与我们的SQLAlchemy模型非常配合,它还处理复杂的任务,如序列化和结果过滤。

第八章测试Flask应用,介绍了如何编写覆盖博客应用程序所有部分的单元测试。我们将利用Flask的测试客户端来模拟“实时”请求。我们还将看到Mock库如何简化测试复杂的交互,如调用数据库等第三方服务。

第九章优秀的扩展,教您如何使用流行的第三方扩展增强您的Flask安装。我们在整本书中都使用了扩展,但现在我们可以探索额外的安全性或功能,而几乎不费吹灰之力,可以很好地完善您的应用程序。

第十章部署您的应用程序,教您如何安全地以自动化、可重复的方式部署您的Flask应用程序。我们将看看如何配置常用的WSGI能力服务器,如Apache和Nginx,以及PythonWeb服务器Gunicorn,为您提供多种选择。然后,我们将看到如何使用SSL安全地部分或整个网站,最后使用配置管理工具来自动化我们的部署。

虽然Python在大多数操作系统上都能很好地运行,而且我们在本书中尽量保持了与操作系统无关的方法,但建议在使用本书时使用运行Linux发行版或OSX的计算机,因为Python已经安装并运行。Linux发行版可以安装在计算机上或虚拟机中。几乎任何Linux发行版都可以,任何最新版本的Ubuntu都可以。

这本书适合任何想要将他们对Python的知识发展成可以在Web上使用的人。Flask遵循Python的设计原则,任何了解Python甚至不了解Python的人都可以轻松理解。

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

文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter用户名显示如下:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

{%blockcontent%}{{entry.body}}Submitacomment{%include"entries/includes/comment_form.html"%}{%endblock%}任何命令行输入或输出都以以下方式书写:

(blog)$pythonmanage.pydbupgradeINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.migration]Runningupgrade594ebac9ef0c->490b6bc5f73c,emptymessage新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“您应该在空白白色页面上看到消息Hello,Flask显示。”

警告或重要提示会出现在这样的框中。

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

Flask很有趣。这是您在查看官方Flask文档时看到的第一件事情之一,而在本书的过程中,您将了解为什么这么多Python开发人员同意这一观点。

在本章中,我们将:

Flask是一个用Python编写的轻量级Web框架。Flask最初是一个愚人节玩笑,后来成为PythonWeb框架世界中备受欢迎的黑马。它现在是创业公司中最广泛使用的PythonWeb框架之一,并且正在成为大多数企业快速简单解决方案的完美工具。在其核心,它提供了一组强大的库,用于处理最常见的Web开发任务,例如:

本书将通过实际的实例教您如何使用这些工具。我们还将讨论Flask中未包含的常用第三方库,例如数据库访问和表单验证。通过本书的学习,您将准备好使用Flask处理下一个大型项目。

正如文档所述,Flask很有趣,但在构建大型应用程序时可能会具有挑战性。与Django等其他流行的PythonWeb框架不同,Flask不强制规定模块或代码的结构方式。如果您有其他Web框架的经验,您可能会惊讶于在Flask中编写应用程序感觉像编写Python而不是框架样板。

本书将教您使用Flask编写清晰、表达力强的应用程序。随着本书的学习,您不仅将成为熟练的Flask开发人员,还将成为更强大的Python开发人员。

您可以通过从命令提示符运行Python交互解释器来验证Python是否已安装并且您拥有正确的版本:

$pythonPython2.7.6(default,Nov262013,12:52:49)[GCC4.8.2]onlinux2Type"help","copyright","credits"or"license"formoreinformation.>>>在提示符(>>>)中键入exit()并按Enter离开解释器。

本书将包含兼容Python2和Python3的代码。不幸的是,由于Python3相对于Python2仍然相对较新,本书中使用的并非所有第三方包都保证与Python3无缝工作。许多人正在努力使流行的开源库与两个版本兼容,但在撰写本文时,仍有一些库尚未移植。为了获得最佳结果,请确保您在系统上安装的Python版本为2.6或更高。

现在您已经确保Python正确安装,我们将安装一些流行的Python包,这些包将在本书的过程中使用。

我们将系统范围内安装这些包,但一旦它们安装完成,我们将专门在虚拟环境中工作。

事实上,Python包安装程序是pip。我们将在整本书中使用它来安装Flask和其他第三方库。

如果您已经安装了setuptools,您可以通过运行以下命令来安装pip:

$sudoeasy_installpip安装完成后,请验证pip是否正确安装:

安装了pip之后,我们可以继续安装任何Python开发人员工具包中最重要的工具:virtualenv。Virtualenv可以轻松创建隔离的Python环境,其中包括它们自己的系统和第三方包的副本。

Virtualenv可以轻松控制项目使用的第三方包的版本。

另一个考虑因素是,通常需要提升权限(sudopipinstallfoo)才能在系统范围内安装包。通过使用virtualenv,您可以创建Python环境并像普通用户一样安装包。如果您正在部署到共享托管环境或者在没有管理员权限的情况下,这将非常有用。

我们将使用pip来安装virtualenv;因为它是一个标准的Python包,所以可以像安装其他Python包一样安装。为了确保virtualenv被系统范围内安装,运行以下命令(需要提升的权限):

现在我们已经安装了适当的工具,我们准备创建我们的第一个Flask应用程序。首先,在一个方便的地方创建一个目录,用于保存所有的Python项目。在命令提示符或终端中,导航到您的项目目录;我的是/home/charles/projects,或者在基于Unix的系统中简写为~/projects。

$mkdir~/projects$cd~/projects现在我们将创建一个virtualenv。下面的命令将在您的项目文件夹中创建一个名为hello_flask的新目录,其中包含一个完整的、隔离的Python环境。

$virtualenvhello_flaskNewpythonexecutableinhello_flask/bin/python2.Alsocreatingexecutableinhello_flask/bin/pythonInstallingsetuptools............done.Installingpip...............done.$cdhello_flask如果列出hello_flask目录的内容,您将看到它创建了几个子目录,包括一个包含Python和pip副本的bin文件夹(在Windows上是Scripts)。下一步是激活您的新virtualenv。具体的说明因使用Windows还是MacOS/Linux而有所不同。要激活您的virtualenv,请参考以下截图:

创建hello_flaskvirtualenv

当您激活一个virtualenv时,您的PATH环境变量会被临时修改,以确保您安装或使用的任何软件包都受限于您的virtualenv。

现在我们已经验证了我们的virtualenv设置正确,我们可以安装Flask了。

当您在虚拟环境中时,永远不应该使用管理员权限安装软件包。如果在尝试安装Flask时收到权限错误,请仔细检查您是否正确激活了您的virtualenv(您的命令提示符中应该看到(hello_flask))。

(hello_flask)$python>>>importflask>>>flask.__version__'0.10.1'>>>flask恭喜!您已经安装了Flask,现在我们准备开始编码。

在hello_flaskvirtualenv中创建一个名为app.py的新文件。使用您喜欢的文本编辑器或IDE,输入以下代码:

fromflaskimportFlaskapp=Flask(__name__)@app.route('/')defindex():return'Hello,Flask!'if__name__=='__main__':app.run(debug=True)保存文件,然后通过命令行运行app.py来执行它。您需要确保已激活hello_flaskvirtualenv:

您的第一个Flask应用程序。

我们刚刚创建了一个非常基本的Flask应用程序。要理解发生了什么,让我们逐行分解这段代码。

fromflaskimportFlask

我们的应用程序通过导入Flask类开始。这个类代表一个单独的WSGI应用程序,是任何Flask项目中的核心对象。

WSGI是Python标准的Web服务器接口,在PEP333中定义。您可以将WSGI视为一组行为和方法,当实现时,允许您的Web应用程序与大量的Web服务器一起工作。Flask为您处理所有实现细节,因此您可以专注于编写Web应用程序。

app=Flask(__name__)

在这一行中,我们在变量app中创建了一个应用程序实例,并将其传递给我们模块的名称。变量app当然可以是任何东西,但是对于大多数Flask应用程序来说,app是一个常见的约定。应用程序实例是诸如视图、URL路由、模板配置等的中央注册表。我们提供当前模块的名称,以便应用程序能够通过查看当前文件夹内部找到资源。这在以后当我们想要渲染模板或提供静态文件时将会很重要。

@app.route('/')defindex():return'Hello,Flask!'在前面的几行中,我们指示我们的Flask应用程序将所有对/(根URL)的请求路由到这个视图函数(index)。视图只是一个返回某种响应的函数或方法。每当您打开浏览器并导航到我们应用程序的根URL时,Flask将调用这个视图函数并将返回值发送到浏览器。

关于这些代码行有一些需要注意的事项:

if__name__=='__main__':app.run(debug=True)路由和请求现在我们的Flask应用程序并不那么有趣,所以让我们看看我们可以以不同方式为我们的Web应用程序添加更有趣的行为。一种常见的方法是添加响应式行为,以便我们的应用程序将查看URL中的值并处理它们。让我们为我们的HelloFlask应用程序添加一个名为hello的新路由。这个新路由将向出现在URL中的人显示问候语:

我们的Flask应用程序显示自定义消息

Flask404页面

除了URL之外,值可以通过查询字符串传递给您的应用程序。查询字符串由任意键和值组成,这些键和值被附加到URL上,使用问号:

为了在视图函数中访问这些值,Flask提供了一个请求对象,该对象封装了关于当前HTTP请求的各种信息。在下面的示例中,我们将修改我们的hello视图,以便通过查询字符串传递的名称也能得到响应。如果在查询字符串或URL中未指定名称,我们将返回404。

fromflaskimportFlask,abort,requestapp=Flask(__name__)@app.route('/')defindex():return'Hello,Flask!'@app.route('/hello/')@app.route('/hello/')defhello(name=None):ifnameisNone:#IfnonameisspecifiedintheURL,attempttoretrieveit#fromthequerystring.name=request.args.get('name')ifname:return'Hello,%s'%nameelse:#NonamewasspecifiedintheURLorthequerystring.abort(404)if__name__=='__main__':app.run(debug=True)正如您所看到的,我们已经为我们的hello视图添加了另一个路由装饰器:Flask允许您将多个URL路由映射到同一个视图。因为我们的新路由不包含名称参数,我们需要修改视图函数的参数签名,使name成为可选参数,我们通过提供默认值None来实现这一点。

我们视图的函数体也已经修改为检查URL中是否存在名称。如果未指定名称,我们将中止并返回404页面未找到状态码。

使用查询字符串问候某人

不可避免的是,迟早我们会在我们的代码中引入一个bug。由于bug是不可避免的,作为开发人员,我们所能希望的最好的事情就是有助于我们快速诊断和修复bug的好工具。幸运的是,Flask自带了一个非常强大的基于Web的调试器。Flask调试器使得在错误发生的瞬间内省应用程序的状态成为可能,消除了需要添加打印语句或断点的必要。

这可以通过在运行时告诉Flask应用程序以debug模式运行来启用。我们可以通过几种方式来做到这一点,但实际上我们已经通过以下代码做到了这一点:

if__name__=='__main__':app.run(debug=True)为了尝试它,让我们通过制造一个拼写错误来引入hello_flask应用程序中的一个bug。在这里,我只是从变量name中简单地删除了末尾的e:

@app.route('/hello/')@app.route('/hello/')defhello(name=None):ifnamisNone:#NonamewasspecifiedintheURLorthequerystring.abort(404)当我们启动开发服务器并尝试访问我们的视图时,现在会出现调试页面:

在Web浏览器中运行的Flask交互式调试器

这个代码列表被称为Traceback,它由调用堆栈组成,即在实际错误之前的嵌套函数调用列表。Traceback通常提供了一个很好的线索,可以解释发生了什么。在底部我们看到了我们有意打错的代码行,以及实际的Python错误,这是一个NameError异常,告诉我们nam未定义。

Traceback详细显示了我们的拼写错误和错误的描述。

真正的魔力发生在你把鼠标放在高亮的行上时。在右侧,你会看到两个小图标,代表终端和源代码文件。点击SourceCode图标将展开包含错误行的源代码。这对于解释错误时建立一些上下文非常有用。

终端图标最有趣。当你点击Terminal图标时,一个小控制台会出现,带有标准的Python提示符。这个提示符允许你实时检查异常发生时本地变量的值。尝试输入name并按Enter——它应该显示在URL中指定的值(如果有的话)。我们还可以通过以下方式检查当前请求参数:

使用调试控制台内省变量

当你在章节中工作并进行实验时,能够快速诊断和纠正任何bug将是一项非常有价值的技能。我们将在第八章中回到交互式调试器,测试Flask应用程序,但现在要知道它的存在,并且可以在代码中断时和地方使用它进行内省。

在本书的其余部分,我们将构建、增强和部署一个对程序员友好的博客站点。这个项目将介绍你最常见的Web开发任务,比如使用关系数据库、处理和验证表单数据,以及(每个人都喜欢的)测试。在每一章中,你将通过实际的、动手编码的项目学习一个新的技能。在下表中,我列出了核心技能的简要描述,以及博客相应的功能:

当开始一个大型项目时,拥有一个功能规范是个好主意。对于博客网站,我们的规范将简单地是我们希望博客具有的功能列表。这些功能是基于我在构建个人博客时的经验:

虽然这个列表并不详尽,但它涵盖了我们博客网站的核心功能,你将有希望发现它既有趣又具有挑战性。在本书的最后,我将提出一些你可能添加的其他功能的想法,但首先你需要熟悉使用Flask。我相信你迫不及待地想要开始,所以让我们设置我们的博客项目。

让我们从在我们的工作目录中创建一个新项目开始;在我的笔记本电脑上是/home/charles/projects,或者在Unix系统中是~/projects,简称为。这正是我们创建hello_flask应用程序时所做的事情:

$cd~/projects$mkdirblog$cdblog然后,我们需要设置我们的virtualenv环境。这与我们之前所做的不同,因为这是一种更有结构的使用虚拟环境的方式:

$virtualenvblog下一步将是将Flask安装到我们的虚拟环境中。为此,我们将激活虚拟环境,并使用pip安装Flask:

$sourceblog/bin/activate(blog)$pipinstallFlask到目前为止,所有这些对你来说应该都有些熟悉。但是,我们可以创建一个名为app的新文件夹,而不是为我们的应用程序创建单个文件,这是完全可以的,对于非常小的应用程序来说是有意义的,这样可以使我们的应用程序模块化和更加合乎逻辑。在该文件夹内,我们将创建五个空文件,分别命名为__init__.py、app.py、config.py、main.py和views.py,如下所示:

我们刚刚创建的这些文件是什么?正如你将看到的,每个文件都有重要的作用。希望它们的名称能够提供关于它们作用的线索,但这里是每个模块责任的简要概述:

让我们用最少量的代码填充这些文件,以创建一个可运行的Flask应用程序。这将使我们的项目在第二章中处于良好的状态,我们将开始编写代码来存储和检索数据库中的博客条目。

我们将从config.py模块开始。这个模块将包含一个Configuration类,指示Flask我们想要在DEBUG模式下运行我们的应用。将以下两行代码添加到config.py模块中:

classConfiguration(object):DEBUG=True接下来我们将创建我们的Flask应用,并指示它使用config模块中指定的配置值。将以下代码添加到app.py模块中:

fromflaskimportFlaskfromconfigimportConfiguration#importourconfigurationdata.app=Flask(__name__)app.config.from_object(Configuration)#usevaluesfromourConfigurationobject.视图模块将包含一个映射到站点根URL的单个视图。将以下代码添加到views.py中:

fromappimportapp@app.route('/')defhomepage():return'Homepage'你可能注意到,我们仍然缺少对app.run()的调用。我们将把这段代码放在main.py中,这将作为我们应用的入口点。将以下代码添加到main.py模块中:

fromappimportapp#importourFlaskappimportviewsif__name__=='__main__':app.run()我们不调用app.run(debug=True),因为我们已经指示Flask在Configuration对象中以调试模式运行我们的应用。

你可以通过执行以下命令行来运行应用程序:

从小小的开始...

除了Configuration类之外,大部分代码对你来说应该很熟悉。我们基本上是将hello_flask示例中的代码分离成了几个模块。可能每个文件只写两三行代码看起来有些愚蠢,但随着我们项目的增长,你会看到这种早期组织的承诺是如何得到回报的。

你可能已经注意到,这些文件有一个内部的优先级,根据它们被导入的顺序—这是为了减轻循环导入的可能性。循环导入发生在两个模块相互导入并且因此根本无法被导入时。在使用Flask框架时,很容易创建循环导入,因为很多不同的东西依赖于中心应用对象。为了避免问题,有些人只是把所有东西放到一个单一的模块中。这对于较小的应用程序来说是可以的,但在一定规模或复杂性之后就无法维护了。这就是为什么我们将我们的应用程序分成几个模块,并创建一个单一的入口点来控制导入的顺序。

当你从命令行运行pythonmain.py时,执行就开始了。Python解释器运行的第一行代码是从app模块导入app对象。现在我们在app.py内部,它导入了Flask和我们的Configuration对象。app.py模块的其余部分被读取和解释,然后我们又回到了main.py。main.py的第二行导入了views模块。现在我们在views.py内部,它依赖于app.py的@app.route,实际上已经从main.py中可用。随着views模块的解释,URL路由和视图被注册,然后我们又回到了main.py。由于我们直接运行main.py,'if'检查将评估为True,我们的应用程序将运行。

执行main.py时的导入流程

到目前为止,你应该已经熟悉了为Python项目设置新的虚拟环境的过程,能够安装Flask,并创建了一个简单的应用程序。在本章中,我们讨论了如何为项目创建虚拟环境,并使用pip安装第三方包。我们还学习了如何编写基本的Flask应用程序,将请求路由到视图,并读取请求参数。我们熟悉了交互式调试器以及Python解释器如何处理导入语句。

如果你已经熟悉本章大部分内容,不用担心;很快事情会变得更具挑战性。

在下一章中,你将了解如何使用关系数据库来存储和检索博客条目。我们将为项目添加一个新模块来存储我们的数据库特定代码,并创建一些模型来表示博客条目和标签。一旦我们能够存储这些条目,我们将学习如何以各种方式通过过滤、排序和聚合来读取它们。更多信息,请参考以下链接:

关系数据库是几乎每个现代Web应用程序构建的基石。学会以表和关系的方式思考你的应用程序是一个干净、设计良好的项目的关键之一。正如你将在本章中看到的,你早期选择的数据模型将影响代码的几乎每个方面。我们将使用SQLAlchemy,一个强大的对象关系映射器,允许我们在Python内部直接与数据库交互,抽象出多个数据库引擎的复杂性。

我们应用程序的数据库远不止是我们需要保存以备将来检索的东西的简单记录。如果我们只需要保存和检索数据,我们可以轻松地使用纯文本文件。事实上,我们希望能够对我们的数据执行有趣的查询。而且,我们希望能够高效地做到这一点,而不需要重新发明轮子。虽然非关系数据库(有时被称为NoSQL数据库)非常受欢迎,并且在Web世界中有其位置,但关系数据库早就解决了过滤、排序、聚合和连接表格数据的常见问题。关系数据库允许我们以结构化的方式定义数据集,从而保持数据的一致性。使用关系数据库还赋予我们开发人员自由,可以专注于我们应用程序中重要的部分。

除了高效执行特别查询外,关系数据库服务器还会执行以下操作:

关系数据库和SQL,与关系数据库一起使用的编程语言,是值得一整本书来讨论的话题。因为这本书致力于教你如何使用Flask构建应用程序,我将向你展示如何使用一个被Python社区广泛采用的用于处理数据库的工具,即SQLAlchemy。

SQLAlchemy是一个在Python中处理关系数据库非常强大的库。我们可以使用普通的Python对象来表示数据库表并执行查询,而不是手动编写SQL查询。这种方法有许多好处,如下所示:

希望您在阅读完这个列表后感到兴奋。如果这个列表中的所有项目现在对您来说都没有意义,不要担心。当您阅读本章和后续章节时,这些好处将变得更加明显和有意义。

现在我们已经讨论了使用SQLAlchemy的一些好处,让我们安装它并开始编码。

我们将使用pip将SQLAlchemy安装到博客应用的虚拟环境中。正如您在上一章中所记得的,要激活您的虚拟环境,只需切换到source并执行activate脚本:

$cd~/projects/blog$sourceblog/bin/activate(blog)$pipinstallsqlalchemyDownloading/unpackingsqlalchemy…SuccessfullyinstalledsqlalchemyCleaningup...您可以通过打开Python解释器并检查SQLAlchemy版本来检查您的安装是否成功;请注意,您的确切版本号可能会有所不同。

$python>>>importsqlalchemy>>>sqlalchemy.__version__'0.9.0b2'在我们的Flask应用中使用SQLAlchemySQLAlchemy在Flask上运行得非常好,但Flask的作者发布了一个名为Flask-SQLAlchemy的特殊Flask扩展,它提供了许多常见任务的辅助功能,并可以避免我们以后不得不重新发明轮子。让我们使用pip来安装这个扩展:

SQLAlchemy支持多种流行的数据库方言,包括SQLite、MySQL和PostgreSQL。根据您想要使用的数据库,您可能需要安装一个包含数据库驱动程序的额外Python包。下面列出了SQLAlchemy支持的一些流行数据库以及相应的pip-installable驱动程序。一些数据库有多个驱动程序选项,所以我首先列出了最流行的一个。

SQLite与Python一起标准提供,并且不需要单独的服务器进程,因此非常适合快速启动。在接下来的示例中,为了简单起见,我将演示如何配置博客应用以使用SQLite。如果您有其他数据库想法,并且希望在博客项目中使用它,请随时使用pip在此时安装必要的驱动程序包。

使用您喜欢的文本编辑器,打开我们博客项目(~/projects/blog/app/config.py)的config.py模块。我们将添加一个特定于SQLAlchemy的设置,以指示Flask-SQLAlchemy如何连接到我们的数据库。以下是新的行:

importosclassConfiguration(object):APPLICATION_DIR=os.path.dirname(os.path.realpath(__file__))DEBUG=TrueSQLALCHEMY_DATABASE_URI='sqlite:///%s/blog.db'%APPLICATION_DIRSQLALCHEMY_DATABASE_URI包括以下部分:

dialect+driver://username:password@host:port/database

因为SQLite数据库存储在本地文件中,我们需要提供的唯一信息是数据库文件的路径。另一方面,如果您想连接到本地运行的PostgreSQL,您的URI可能看起来像这样:

postgresql://postgres:secretpassword@localhost:5432/blog_db

现在我们已经指定了如何连接到数据库,让我们创建一个负责实际管理我们数据库连接的对象。这个对象由Flask-SQLAlchemy扩展提供,并且方便地命名为SQLAlchemy。打开app.py并进行以下添加:

fromflaskimportFlaskfromflask.ext.sqlalchemyimportSQLAlchemyfromconfigimportConfigurationapp=Flask(__name__)app.config.from_object(Configuration)db=SQLAlchemy(app)这些更改指示我们的Flask应用程序,进而指示SQLAlchemy如何与我们应用程序的数据库通信。下一步将是创建一个用于存储博客条目的表,为此,我们将创建我们的第一个模型。

模型是我们想要存储在数据库中的数据表的数据表示。这些模型具有称为列的属性,表示数据中的数据项。因此,如果我们要创建一个Person模型,我们可能会有用于存储名字、姓氏、出生日期、家庭地址、头发颜色等的列。由于我们有兴趣创建一个模型来表示博客条目,我们将为标题和正文内容等内容创建列。

请注意,我们不说People模型或Entries模型-即使它们通常代表许多不同的对象,模型是单数。

使用SQLAlchemy,创建模型就像定义一个类并指定分配给该类的多个属性一样简单。让我们从我们博客条目的一个非常基本的模型开始。在博客项目的app/目录中创建一个名为models.py的新文件,并输入以下代码:

在Entry模型之前,我们定义了一个辅助函数slugify,我们将使用它为我们的博客条目提供一些漂亮的URL(在第三章中使用,模板和视图)。slugify函数接受一个字符串,比如关于Flask的帖子,并使用正则表达式将可读的字符串转换为URL,因此返回a-post-about-flask。

接下来是Entry模型。我们的Entry模型是一个普通的类,扩展了db.Model。通过扩展db.Model,我们的Entry类将继承各种我们将用于查询数据库的帮助程序。

Entry模型的属性是我们希望存储在数据库中的名称和数据的简单映射,并列在下面:

对于标题或事物名称等短字符串,String列是合适的,但当文本可能特别长时,最好使用Text列,就像我们为条目正文所做的那样。

我们已经重写了类的构造函数(__init__),这样,当创建一个新模型时,它会根据标题自动为我们设置slug。

最后一部分是__repr__方法,用于生成我们的Entry类实例的有用表示。__repr__的具体含义并不重要,但允许你在调试时引用程序正在处理的对象。

最后需要添加一小段代码到main.py,这是我们应用程序的入口点,以确保模型被导入。将以下突出显示的更改添加到main.py中:

fromappimportapp,dbimportmodelsimportviewsif__name__=='__main__':app.run()创建Entry表为了开始使用Entry模型,我们首先需要在我们的数据库中为它创建一个表。幸运的是,Flask-SQLAlchemy带有一个很好的辅助程序来做这件事。在博客项目的app目录中创建一个名为scripts的新子文件夹。然后创建一个名为create_db.py的文件:

(blog)$cdapp/(blog)$mkdirscripts(blog)$touchscripts/create_db.py将以下代码添加到create_db.py模块中。这个函数将自动查看我们编写的所有代码,并根据我们的模型在数据库中为Entry模型创建一个新表:

importos,syssys.path.append(os.getcwd())frommainimportdbif__name__=='__main__':db.create_all()从app/目录内执行脚本。确保虚拟环境是激活的。如果一切顺利,你应该看不到任何输出。

(blog)$pythoncreate_db.py(blog)$注意如果在创建数据库表时遇到错误,请确保你在app目录中,并且在运行脚本时虚拟环境是激活的。接下来,确保你的SQLALCHEMY_DATABASE_URI设置中没有拼写错误。

让我们通过保存一些博客条目来尝试我们的新Entry模型。我们将在Python交互式shell中进行此操作。在这个阶段,让我们安装IPython,这是一个功能强大的shell,具有诸如制表符补全(默认的Pythonshell没有的功能)。

(blog)$pipinstallipython现在检查我们是否在app目录中,让我们启动shell并创建一些条目,如下所示:

(blog)$ipythonIn[]:frommodelsimport*#Firstthingsfirst,importourEntrymodelanddbobject.In[]:db#WhatisdbOut[]:注意如果你熟悉普通的Pythonshell但不熟悉IPython,一开始可能会有点不同。要注意的主要事情是In[]指的是你输入的代码,Out[]是你放入shell的命令的输出。

IPython有一个很棒的功能,允许你打印关于对象的详细信息。这是通过输入对象的名称后跟一个问号()来完成的。内省Entry模型提供了一些信息,包括参数签名和表示该对象的字符串(称为docstring)的构造函数。

In[]:Entry#WhatisEntryandhowdowecreateitType:_BoundDeclarativeMetaStringForm:File:/home/charles/projects/blog/app/models.pyDocstring:Constructorinformation:Definition:Entry(self,*args,**kwargs)我们可以通过将列值作为关键字参数传递来创建Entry对象。在前面的示例中,它使用了**kwargs;这是一个快捷方式,用于将dict对象作为定义对象的值,如下所示:

In[]:first_entry=Entry(title='Firstentry',body='Thisisthebodyofmyfirstentry.')为了保存我们的第一个条目,我们将其添加到数据库会话中。会话只是表示我们在数据库上的操作的对象。即使将其添加到会话中,它也不会立即保存到数据库中。为了将条目保存到数据库中,我们需要提交我们的会话:

尝试自己添加几个。在提交之前,您可以将多个条目对象添加到同一个会话中,因此也可以尝试一下。

在您进行实验的任何时候,都可以随时删除blog.db文件,并重新运行create_db.py脚本,以便使用全新的数据库重新开始。

修改现有的Entry时,只需进行编辑,然后提交。让我们使用之前返回给我们的id检索我们的Entry,进行一些更改,然后提交。SQLAlchemy将知道需要更新它。以下是您可能对第一个条目进行编辑的方式:

In[]:first_entry=Entry.query.get(1)In[]:first_entry.body='Thisisthefirstentry,andIhavemadesomeedits.'In[]:db.session.commit()就像那样,您的更改已保存。

删除条目与创建条目一样简单。我们将调用db.session.delete而不是调用db.session.add,并传入我们希望删除的Entry实例。

In[]:bad_entry=Entry(title='badentry',body='Thisisalousyentry.')In[]:db.session.add(bad_entry)In[]:db.session.commit()#Savethebadentrytothedatabase.In[]:db.session.delete(bad_entry)In[]:db.session.commit()#Thebadentryisnowdeletedfromthedatabase.检索博客条目虽然创建、更新和删除操作相当简单,但当我们查看检索条目的方法时,真正有趣的部分开始了。我们将从基础知识开始,然后逐渐深入到更有趣的查询。

我们将使用模型类上的特殊属性进行查询:Entry.query。该属性公开了各种API,用于处理数据库中条目的集合。

让我们简单地检索Entry表中所有条目的列表:

In[]:entries=Entry.query.all()In[]:entries#WhatareourentriesOut[]:[,,,]如您所见,在此示例中,查询返回了我们创建的Entry实例的列表。当未指定显式排序时,条目将以数据库选择的任意顺序返回给我们。让我们指定我们希望以标题的字母顺序返回给我们条目:

In[]:oldest_to_newest=Entry.query.order_by(Entry.modified_timestamp.desc()).all()Out[]:[,,,]过滤条目列表能够检索整个博客条目集合非常有用,但是如果我们想要过滤列表怎么办?我们可以始终检索整个集合,然后在Python中使用循环进行过滤,但那将非常低效。相反,我们将依赖数据库为我们进行过滤,并简单地指定应返回哪些条目的条件。在以下示例中,我们将指定要按标题等于'Firstentry'进行过滤的条目。

In[]:Entry.query.filter(Entry.title=='Firstentry').all()Out[]:[]如果这对您来说似乎有些神奇,那是因为它确实如此!SQLAlchemy使用操作符重载将诸如.==的表达式转换为称为BinaryExpression的抽象对象。当您准备执行查询时,这些数据结构然后被转换为SQL。

BinaryExpression只是一个表示逻辑比较的对象,并且是通过重写通常在Python中比较值时调用的标准方法而生成的。

为了检索单个条目,您有两个选项:.first()和.one()。它们的区别和相似之处总结在以下表中:

让我们尝试与之前相同的查询,但是,而不是调用.all(),我们将调用.first()来检索单个Entry实例:

In[]:Entry.query.filter(Entry.title=='Firstentry').first()Out[]:请注意,以前的.all()返回包含对象的列表,而.first()只返回对象本身。

在前面的示例中,我们测试了相等性,但还有许多其他类型的查找可能。在下表中,我们列出了一些您可能会发现有用的查找。完整列表可以在SQLAlchemy文档中找到。

前面表格中列出的表达式可以使用位运算符组合,以生成任意复杂的表达式。假设我们想要检索所有博客条目中标题包含Python或Flask的条目。为了实现这一点,我们将创建两个contains表达式,然后使用Python的位OR运算符进行组合,这是一个管道|字符,不像其他许多使用双管||字符的语言:

Entry.query.filter(Entry.title.contains('Python')|Entry.title.contains('Flask'))使用位运算符,我们可以得到一些非常复杂的表达式。试着弄清楚以下示例在询问什么:

Entry.query.filter((Entry.title.contains('Python')|Entry.title.contains('Flask'))&(Entry.created_timestamp>(datetime.date.today()-datetime.timedelta(days=30))))您可能已经猜到,此查询返回所有标题包含Python或Flask的条目,并且在过去30天内创建。我们使用Python的位OR和AND运算符来组合子表达式。对于您生成的任何查询,可以通过打印查询来查看生成的SQL,如下所示:

In[]:query=Entry.query.filter((Entry.title.contains('Python')|Entry.title.contains('Flask'))&(Entry.created_timestamp>(datetime.date.today()-datetime.timedelta(days=30))))In[]:printstr(query)SELECTentry.idASentry_id,...FROMentryWHERE((entry.titleLIKE'%%'||:title_1||'%%')OR(entry.titleLIKE'%%'||:title_2||'%%'))ANDentry.created_timestamp>:created_timestamp_1否定还有一点要讨论,那就是否定。如果我们想要获取所有标题中不包含Python或Flask的博客条目列表,我们该怎么做呢?SQLAlchemy提供了两种方法来创建这些类型的表达式,一种是使用Python的一元否定运算符(~),另一种是调用db.not_()。以下是如何使用SQLAlchemy构建此查询的方法:

使用一元否定:

In[]:Entry.query.filter(~(Entry.title.contains('Python')|Entry.title.contains('Flask')))使用db.not_():

In[]:Entry.query.filter(db.not_(Entry.title.contains('Python')|Entry.title.contains('Flask')))运算符优先级并非所有操作都被Python解释器视为相等。这就像在数学课上学习的那样,我们学到类似2+3*4的表达式等于14而不是20,因为乘法运算首先发生。在Python中,位运算符的优先级都高于诸如相等性测试之类的东西,这意味着在构建查询表达式时,您必须注意括号。让我们看一些示例Python表达式,并查看相应的查询:

如果您发现自己在操作符优先级方面有困难,最好在使用==、!=、<、<=、>和>=的任何比较周围加上括号。

为了对此进行建模,我们必须首先创建一个模型来存储标签。这个模型将存储我们使用的标签名称,因此在我们添加了一些标签之后,表可能看起来像下面这样:

让我们打开models.py并为Tag模型添加一个定义。在文件末尾添加以下类,位于Entry类下方:

classTag(db.Model):id=db.Column(db.Integer,primary_key=True)name=db.Column(db.String(64))slug=db.Column(db.String(64),unique=True)def__init__(self,*args,**kwargs):super(Tag,self).__init__(*args,**kwargs)self.slug=slugify(self.name)def__repr__(self):return''%self.name您以前见过所有这些。我们添加了一个主键,这将由数据库管理,并添加了一个列来存储标签的名称。name列被标记为唯一,因此每个标签在这个表中只会被一行表示,无论它出现在多少个博客条目中。

现在我们既有博客条目模型,也有标签模型,我们需要一个第三个模型来存储两者之间的关系。当我们希望表示博客条目被标记为特定标签时,我们将在这个表中存储一个引用。以下是数据库表级别上正在发生的事情的图示:

由于我们永远不会直接访问这个中间表(SQLAlchemy会透明地处理它),我们不会为它创建一个模型,而是简单地指定一个表来存储映射。打开models.py并添加以下突出显示的代码:

让我们使用IPythonshell来看看这是如何工作的。关闭当前的shell并重新运行scripts/create_db.py脚本。由于我们添加了两个新表,这一步是必要的。现在重新打开IPython:

(blog)$pythonscripts/create_db.py(blog)$ipythonIn[]:frommodelsimport*In[]:Tag.query.all()Out[]:[]目前数据库中没有标签,所以让我们创建一些标签:

In[]:python=Tag(name='python')In[]:flask=Tag(name='flask')In[]:db.session.add_all([python,flask])In[]:db.session.commit()现在让我们加载一些示例条目。在我的数据库中有四个:

In[]:Entry.query.all()Out[]:[,,,]In[]:python_entry,flask_entry,more_flask,django_entry=_注意在IPython中,您可以使用下划线(_)来引用上一行的返回值。

要向条目添加标签,只需将它们分配给条目的tags属性。就是这么简单!

In[]:python_entry.tags=[python]In[]:flask_entry.tags=[python,flask]In[]:db.session.commit()我们可以像处理普通的Python列表一样处理条目的标签列表,因此通常的.append()和.remove()方法也可以使用:

In[]:kittens=Tag(name='kittens')In[]:python_entry.tags.append(kittens)In[]:db.session.commit()In[]:python_entry.tagsOut[]:[,]In[]:python_entry.tags.remove(kittens)In[]:db.session.commit()In[]:python_entry.tagsOut[]:[]使用backrefs创建Entry模型上的tags属性时,您会回忆起我们传入了backref参数。让我们使用IPython来看看后向引用是如何使用的。

In[]:python#Thepythonvariableisjustatag.Out[]:In[]:python.entriesOut[]:In[]:python.entries.all()Out[]:[,]与Entry.tags引用不同,后向引用被指定为lazy='dynamic'。这意味着,与给出标签列表的entry.tags不同,我们每次访问tag.entries时都不会收到条目列表。为什么呢?通常,当结果集大于几个项目时,将backref参数视为查询更有用,可以进行过滤、排序等操作。例如,如果我们想显示最新的标记为python的条目会怎样?

本章最后要讨论的主题是如何对现有的模型定义进行修改。根据项目规范,我们希望能够保存博客条目的草稿。现在我们没有办法知道一个条目是否是草稿,所以我们需要添加一个列来存储条目的状态。不幸的是,虽然db.create_all()用于创建表非常完美,但它不会自动修改现有的表;为了做到这一点,我们需要使用迁移。

我们将使用Flask-Migrate来帮助我们在更改模式时自动更新数据库。在博客虚拟环境中,使用pip安装Flask-Migrate:

(blog)$pipinstallflask-migrate注意SQLAlchemy的作者有一个名为alembic的项目;Flask-Migrate使用它并直接将其与Flask集成,使事情变得更容易。

接下来,我们将向我们的应用程序添加一个Migrate助手。我们还将为我们的应用程序创建一个脚本管理器。脚本管理器允许我们在应用程序的上下文中直接从命令行执行特殊命令。我们将使用脚本管理器来执行migrate命令。打开app.py并进行以下添加:

fromflaskimportFlaskfromflask.ext.migrateimportMigrate,MigrateCommandfromflask.ext.scriptimportManagerfromflask.ext.sqlalchemyimportSQLAlchemyfromconfigimportConfigurationapp=Flask(__name__)app.config.from_object(Configuration)db=SQLAlchemy(app)migrate=Migrate(app,db)manager=Manager(app)manager.add_command('db',MigrateCommand)为了使用管理器,我们将在app.py旁边添加一个名为manage.py的新文件。将以下代码添加到manage.py中:

fromappimportmanagerfrommainimport*if__name__=='__main__':manager.run()这看起来与main.py非常相似,关键区别在于,我们不是调用app.run(),而是调用manager.run()。

Django有一个类似的,尽管是自动生成的manage.py文件,起着类似的功能。

在我们开始更改模式之前,我们需要创建其当前状态的记录。为此,请从博客的app目录内运行以下命令。第一个命令将在app文件夹内创建一个迁移目录,用于跟踪我们对模式所做的更改。第二个命令dbmigrate将创建我们当前模式的快照,以便将来的更改可以与之进行比较。

(blog)$pythonmanage.pydbinitCreatingdirectory/home/charles/projects/blog/app/migrations...done...(blog)$pythonmanage.pydbmigrateINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.Generating/home/charles/projects/blog/app/migrations/versions/535133f91f00_.py...done最后,我们将运行dbupgrade来运行迁移,以指示迁移系统一切都是最新的:

(blog)$pythonmanage.pydbupgradeINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.migration]RunningupgradeNone->535133f91f00,emptymessage添加状态列现在我们已经有了当前模式的快照,我们可以开始进行更改。我们将添加一个名为status的新列,该列将存储与特定状态对应的整数值。尽管目前只有两种状态(PUBLIC和DRAFT),但使用整数而不是布尔值使我们有可能在将来轻松添加更多状态。打开models.py并对Entry模型进行以下添加:

classEntry(db.Model):STATUS_PUBLIC=0STATUS_DRAFT=1id=db.Column(db.Integer,primary_key=True)title=db.Column(db.String(100))slug=db.Column(db.String(100),unique=True)body=db.Column(db.Text)status=db.Column(db.SmallInteger,default=STATUS_PUBLIC)created_timestamp=db.Column(db.DateTime,default=datetime.datetime.now)...从命令行,我们将再次运行dbmigrate来生成迁移脚本。您可以从命令的输出中看到它找到了我们的新列!

(blog)$pythonmanage.pydbmigrateINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.autogenerate.compare]Detectedaddedcolumn'entry.status'Generating/home/charles/projects/blog/app/migrations/versions/2c8e81936cad_.py...done因为我们在数据库中有博客条目,所以我们需要对自动生成的迁移进行小修改,以确保现有条目的状态被初始化为正确的值。为此,打开迁移文件(我的是migrations/versions/2c8e81936cad_.py)并更改以下行:

op.add_column('entry',sa.Column('status',sa.SmallInteger(),nullable=True))将nullable=True替换为server_default='0'告诉迁移脚本不要将列默认设置为null,而是使用0。

op.add_column('entry',sa.Column('status',sa.SmallInteger(),server_default='0'))最后,运行dbupgrade来运行迁移并创建状态列。

(blog)$pythonmanage.pydbupgradeINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.migration]Runningupgrade535133f91f00->2c8e81936cad,emptymessage恭喜,您的Entry模型现在有了一个状态字段!

到目前为止,您应该熟悉使用SQLAlchemy来处理关系数据库。我们介绍了使用关系数据库和ORM的好处,配置了一个Flask应用程序来连接到关系数据库,并创建了SQLAlchemy模型。所有这些都使我们能够在数据之间创建关系并执行查询。最重要的是,我们还使用了迁移工具来处理未来的数据库模式更改。

在第三章中,模板和视图,我们将搁置交互式解释器,开始创建视图以在Web浏览器中显示博客条目。我们将利用我们所有的SQLAlchemy知识创建有趣的博客条目列表,以及一个简单的搜索功能。我们将构建一组模板,使博客网站在视觉上更具吸引力,并学习如何使用Jinja2模板语言来消除重复的HTML编码。这将是一个有趣的章节!

这一章也可以被称为Flask章节,因为我们将涵盖框架中最具代表性的两个组件:Jinja2模板语言和URL路由框架。到目前为止,我们一直在为博客应用奠定基础,但实际上我们几乎没有涉及到Flask的开发。在这一章中,我们将深入了解Flask,并看到我们的应用最终开始成形。我们将把单调的数据库模型转换为动态呈现的HTML页面,使用模板。我们将设计一个URL方案,反映我们希望组织博客条目的方式。随着我们在本章的进展,我们的博客应用将开始看起来像一个真正的网站。

从一开始,Flask就是以Jinja2为核心构建的,因此在Flask应用中使用模板非常容易。由于Jinja2是Flask框架的要求,它已经安装在我们的虚拟环境中,所以我们可以立即开始使用。

在博客项目的app目录中创建一个名为templates的新文件夹。在模板文件夹中创建一个名为homepage.html的单个文件,并添加以下HTML代码:

Blog

Welcometomyblog

现在在博客项目的app目录中打开views.py。我们将修改我们的homepage视图以呈现新的homepage.html模板。为此,我们将使用Flask的render_template()函数,将我们的模板名称作为第一个参数传递进去。呈现模板是一个非常常见的操作,所以Flask尽可能地简化了这部分内容:

前面的例子可能看起来并不那么令人印象深刻,因为我们所做的不过是提供一个简单的HTML文档。为了使事情变得有趣,我们需要给我们的模板提供上下文。让我们修改我们的主页,显示一个简单的问候语来说明这一点。打开views.py并进行以下修改:

fromflaskimportrender_template,requestfromappimportapp@app.route('/')defhomepage():name=request.args.get('name')ifnotname:name=''returnrender_template('homepage.html',name=name)在视图代码中,我们将name传递到模板上下文中。下一步是在实际模板中对name做一些操作。在这个例子中,我们将简单地打印name的值。打开homepage.html并进行以下添加:

Blog

Welcometomyblog

Yournameis{{name}}.

启动开发服务器并导航到根URL。你应该看到类似下面图片的东西:

传递给render_template函数的任何关键字参数都可以在模板上下文中使用。在Jinja2的模板语言中,双大括号类似于print语句。我们使用{{name}}操作来输出name的值,该值设置为

注重安全的读者可能已经注意到,当我们在浏览器中查看我们的模板时,括号被转义了。通常,括号被浏览器视为HTML标记,但是如您所见,Jinja2已经自动转义了括号,用<和>替换了它们。

基本模板操作

假设有人恶意访问您的网站并想要制造一些麻烦。注意到查询字符串中的值直接传递到模板中,这个人决定尝试注入一个脚本标记来玩一些恶作剧。幸运的是,Jinja2在将值插入渲染页面之前会自动转义这些值。

Jinja2支持一种微型编程语言,可用于在上下文中对数据执行操作。如果我们只能将值打印到上下文中,那么实际上就没有太多令人兴奋的事情了。当我们将上下文数据与循环和控制结构等内容结合在一起时,事情就变得有趣起来了。

让我们再次修改我们的主页视图。这次我们将从request.args中接受一个数字,以及一个名称,并显示0到该数字之间的所有偶数。好处是我们几乎可以在模板中完成所有这些工作。对views.py进行以下更改:

fromflaskimportrender_template,requestfromappimportapp@app.route('/')defhomepage():name=request.args.get('name')number=request.args.get('number')returnrender_template('homepage.html',name=name,number=number)现在打开hompage.html模板并添加以下代码。如果看起来奇怪,不用担心。我们将逐行讲解。

Blog

Welcometomyblog

{%ifnumber%}

Yournumberis{{number|int}}

    {%foriinrange(number|int)%}{%ifiisdivisibleby2%}
  • {{i}}
  • {%endif%}{%endfor%}
{%else%}

Nonumberspecified.

{%endif%}

Yournameis{{name|default('',True)}}.

启动runserver并通过查询字符串传递一些值进行实验。还要注意当传递非数字值或负值时会发生什么。

循环、控制结构和模板编程

让我们逐行讲解我们的新模板代码,从{%ifnumber%}语句开始。与使用双大括号的打印标记不同,逻辑标记使用{%和%}。我们只是检查上下文中是否传递了一个数字。如果数字是None或空字符串,则此测试将失败,就像在Python中一样。

下一行打印了我们数字的整数表示,并使用了一个新的语法|int。竖线符号(|)在Jinja2中用于表示对过滤器的调用。过滤器对位于竖线符号左侧的值执行某种操作,并返回一个新值。在这种情况下,我们使用了内置的int过滤器,将字符串转换为整数,在无法确定数字时默认为0。Jinja2内置了许多过滤器;我们将在本章后面讨论它们。

{%for%}语句用于创建一个for循环,看起来非常接近Python。我们使用Jinja2的range辅助函数生成一个数字序列[0,number)。请注意,我们再次通过int过滤器在调用range时将number上下文值传递给range。还要注意,我们将一个值赋给一个新的上下文变量i。在循环体内,我们可以像使用任何其他上下文变量一样使用i。

当然,就像在普通的Python中一样,我们也可以在for循环上使用{%else%}语句,用于在没有要执行的循环时运行一些代码。

现在我们正在循环遍历数字,我们需要检查i是否为偶数,如果是,则打印出来。Jinja2提供了几种方法可以做到这一点,但我选择展示了一种名为tests的Jinja2特性的使用。与过滤器和控制结构一样,Jinja2还提供了许多有用的工具来测试上下文值的属性。测试与{%if%}语句一起使用,并通过关键字is表示。因此,我们有{%ifiisdivisibleby2%},这非常容易阅读。如果if语句评估为True,那么我们将使用双大括号打印i的值:{{i}}。

由于Jinja2不知道重要的空格,我们需要明确关闭所有逻辑标记。这就是为什么您看到了一个{%endif%}标记,表示divisibleby2检查的关闭,以及一个{%endfor%}标记,表示foriinrange循环的关闭。在for循环之后,我们现在处于最外层的if语句中,该语句测试是否将数字传递到上下文中。如果没有数字存在,我们希望向用户显示一条消息,因此在调用{%endif%}之前,我们将使用{%else%}标记来显示此消息。

最后,我们已将打印向用户问候语的行更改为{{name|default('',True)}}。在视图代码中,我们删除了将其设置为默认值的逻辑。相反,我们将该逻辑移到了模板中。在这里,我们看到了default过滤器(由|字符表示),但与int不同的是,我们传递了多个参数。在Jinja2中,过滤器可以接受多个参数。按照惯例,第一个参数出现在管道符号的左侧,因为过滤器经常操作单个值。如果有多个参数,则这些参数在过滤器名称之后的括号中指定。在default过滤器的情况下,我们已指定在未指定名称时使用的值。

在以下示例中,参数列表中的第一个参数将出现在管道符号的左侧。因此,即使我写了abs(number),使用的过滤器将是number|abs。当过滤器接受多个参数时,剩余的参数将在过滤器名称后的括号中显示。

|default(value,default_value='',boolean=False)|如果value未定义(即上下文中不存在该名称),则使用提供的default_value。如果您只想测试value是否评估为布尔值True(即不是空字符串,数字零,None等),则将第三个参数传递为True:

{{not_in_context|default:"Thevaluewasnotinthecontext"}}{{''|default('Anemptystring.',True)}}|

|dictsort(value,case_sensitive=False,by='key')|按键对字典进行排序,产生(key,value)对。但您也可以按值排序。

Alphabeticallybyname.

{%forname,ageinpeople|dictsort%}{{name}}is{{age}}yearsold.{%endfor%}

Youngesttooldest.

{%forname,ageinpeople|dictsort(by='value')%}{{name}}is{{age}}yearsold.{%endfor%}|

|safe(value)|输出未转义的值。当您有信任的HTML希望打印时,此过滤器非常有用。例如,如果value="":

{{value}}-->outputs<b>{{value|safe}}-->outputs|

过滤器可以链接在一起,所以{{number|int|abs}}首先将数字变量转换为整数,然后返回其绝对值。

Jinja2的继承和包含功能使得定义一个基础模板成为站点上每个页面的架构基础非常容易。基础模板包含一些基本结构,如、和标签,以及body的基本结构。它还可以用于包含样式表或脚本,这些样式表或脚本将在每个页面上提供。最重要的是,基础模板负责定义可覆盖的块,我们将在其中放置特定于页面的内容,如页面标题和正文内容。

为了快速启动,我们将使用Twitter的Bootstrap库(版本3)。这将使我们能够专注于模板的结构,并且只需进行最少的额外工作就能拥有一个看起来不错的网站。当然,如果您愿意,也可以使用自己的CSS,但示例代码将使用特定于bootstrap的结构。

在templates目录中创建一个名为base.html的新文件,并添加以下内容:

您可能已经注意到我们正在从公开可用的URL中提供jQuery和Bootstrap。在下一章中,我们将讨论如何提供存储在本地磁盘上的静态文件。现在我们可以修改我们的主页模板,并利用新的基础模板。我们可以通过扩展基础模板并覆盖某些块来实现这一点。这与大多数语言中的类继承非常相似。只要继承页面的部分被很好地分成块,我们就可以只覆盖需要更改的部分。让我们打开homepage.html,并用以下内容替换当前内容的一部分:

我们仍然需要构建模板来显示我们的博客条目。但在继续构建模板之前,我们首先必须创建一些视图函数,这些函数将生成博客条目的列表。然后我们将条目传递到上下文中,就像我们在主页中所做的那样。

URL是给人看的,因此它们应该易于记忆。当URL方案准确反映网站的隐含结构时,良好的URL方案易于记忆。我们的目标是创建一个URL方案,使我们网站上的访问者能够轻松找到他们感兴趣的主题的博客条目。

参考我们在第一章中创建的规范,创建您的第一个Flask应用程序,我们知道我们希望我们的博客条目按标签和日期进行组织。按标签和日期组织的条目必然是所有条目的子集,因此给我们提供了这样的结构:

/entries/learning-the-flask-framework/

/entries/2014/jan/18/learning-the-flask-framework/

让我们将之前描述的结构转换为Flask将理解的一些URL路由。在博客项目的app目录中创建一个名为entries的新目录。在entries目录内,创建两个文件,__init__.py和blueprint.py如下:

fromflaskimportBlueprintfrommodelsimportEntry,Tagentries=Blueprint('entries',__name__,template_folder='templates')@entries.route('/')defindex():return'Entriesindex'@entries.route('/tags/')deftag_index():pass@entries.route('/tags//')deftag_detail(slug):pass@entries.route('//')defdetail(slug):pass这些URL路由是我们将很快填充的占位符,但我想向您展示如何将一组URL模式清晰简单地转换为一组路由和视图。

为了访问这些新视图,我们需要使用我们的主要Flaskapp对象注册我们的blueprint。我们还将指示我们的应用程序,我们希望我们的条目的URL位于前缀/entries。打开main.py并进行以下添加:

fromflaskimportrender_template,requestdefobject_list(template_name,query,paginate_by=20,**context):page=request.args.get('page')ifpageandpage.isdigit():page=int(page)else:page=1object_list=query.paginate(page,paginate_by)returnrender_template(template_name,object_list=object_list,**context)现在,我们将打开entries/blueprint.py并修改index视图以返回分页列表条目:

fromflaskimportBlueprintfromhelpersimportobject_listfrommodelsimportEntry,Tagentries=Blueprint('entries',__name__,template_folder='templates')@entries.route('/')defindex():entries=Entry.query.order_by(Entry.created_timestamp.desc())returnobject_list('entries/index.html',entries)我们正在导入object_list辅助函数,并将其传递给模板的名称和表示我们希望显示的条目的查询。随着我们构建这些视图的其余部分,您将看到诸如object_list这样的小辅助函数如何使Flask开发变得非常容易。

最后一部分是entries/index.html模板。在entries目录中,创建一个名为templates的目录,和一个名为entries的子目录。创建index.html,使得从app目录到entries/templates/entries/index.html的完整路径,并添加以下代码:

{%extends"base.html"%}{%blocktitle%}Entries{%endblock%}{%blockcontent_title%}Entries{%endblock%}{%blockcontent%}{%include"includes/list.html"%}{%endblock%}这个模板非常简单,所有的工作都将在includes/list.html中进行。{%include%}标签是新的,对于可重用的模板片段非常有用。创建文件includes/list.html并添加以下代码:

在构建详细视图之前,重新打开基本模板,并在导航部分添加一个链接到条目:

让我们创建一个简单的视图,用于呈现单个博客条目的内容。条目的slug将作为URL的一部分传递进来。我们将尝试将其与现有的Entry匹配,如果没有匹配项,则返回404响应。更新entriesblueprint中的detail视图的以下代码:

fromflaskimportrender_template@entries.route('//')defdetail(slug):entry=Entry.query.filter(Entry.slug==slug).first_or_404()returnrender_template('entries/detail.html',entry=entry)在entries模板目录中创建一个名为detail.html的模板,并添加以下代码。我们将在主内容区域显示条目的标题和正文,但在侧边栏中,我们将显示一个标签列表和条目创建日期:

列出与给定标签匹配的条目将结合两个先前视图的逻辑。首先,我们需要使用URL中提供的tagslug查找Tag,然后我们将显示一个object_list,其中包含使用指定标签标记的Entry对象。在tag_detail视图中,添加以下代码:

{%extends"base.html"%}{%blocktitle%}{{tag.name}}entries{%endblock%}{%blockcontent_title%}{{tag.name}}entries{%endblock%}{%blockcontent%}{%include"includes/list.html"%}{%endblock%}在下面的屏幕截图中,我已经导航到/entries/tags/python/。这个页面只包含已经被标记为Python的条目:

最后缺失的部分是显示所有标签列表的视图。这个视图将与index条目非常相似,只是我们将查询Tag模型而不是Entry对象。更新以下代码到tag_index视图:

@entries.route('/tags/')deftag_index():tags=Tag.query.order_by(Tag.name)returnobject_list('entries/tag_index.html',tags)在模板中,我们将每个标签显示为指向相应标签详情页面的链接。创建文件entries/tag_index.html并添加以下代码:

为了让用户能够找到包含特定单词或短语的帖子,我们将在包含博客条目列表的页面上添加简单的全文搜索。为了实现这一点,我们将进行一些重构。我们将在所有包含博客条目列表的页面的侧边栏中添加一个搜索表单。虽然我们可以将相同的代码复制粘贴到entries/index.html和entries/tag_detail.html中,但我们将创建另一个包含搜索小部件的基础模板。创建一个名为entries/base_entries.html的新模板,并添加以下代码:

现在我们将更新entries/index.html和entries/tag_detail.html以利用这个新的基础模板。由于content块包含条目列表,我们可以从这两个模板中删除它:

{%extends"entries/base_entries.html"%}{%blocktitle%}Entries{%endblock%}{%blockcontent_title%}Entries{%endblock%}这是在更改基础模板并删除上下文块后的entries/index.html的样子。对entries/tag_detail.html做同样的操作。

{%extends"entries/base_entries.html"%}{%blocktitle%}Tags{%endblock%}{%blockcontent_title%}Tags{%endblock%}现在我们需要更新我们的视图代码来实际执行搜索。为此,我们将在蓝图中创建一个名为entry_list的新辅助函数。这个辅助函数将类似于object_list辅助函数,但会执行额外的逻辑来根据我们的搜索查询过滤结果。将entry_list函数添加到blueprint.py中。注意它如何检查请求查询字符串是否包含名为q的参数。如果q存在,我们将只返回标题或正文中包含搜索短语的条目:

fromflaskimportrequestdefentry_list(template,query,**context):search=request.args.get('q')ifsearch:query=query.filter((Entry.body.contains(search))|(Entry.title.contains(search)))returnobject_list(template,query,**context)为了利用这个功能,修改index和tag_detail视图,调用entry_list而不是object_list。更新后的index视图如下:

@entries.route('/')defindex():entries=Entry.query.order_by(Entry.created_timestamp.desc())returnentry_list('entries/index.html',entries)恭喜!现在你可以导航到条目列表并使用搜索表单进行搜索。

正如我们之前讨论的,我们希望对条目的长列表进行分页,以便用户不会被极长的列表所压倒。我们实际上已经在object_list函数中完成了所有工作;唯一剩下的任务是添加链接,让用户可以从一个条目页面跳转到下一个页面。

因为分页链接是我们将在多个地方使用的一个功能,我们将在应用程序的模板目录中创建分页include(而不是条目模板目录)。在app/templates/中创建一个名为includes的新目录,并创建一个名为page_links.html的文件。由于object_list返回一个PaginatedQuery对象,我们可以在模板中利用这个对象来确定我们所在的页面以及总共有多少页。为了使分页链接看起来漂亮,我们将使用Bootstrap提供的CSS类。将以下内容添加到page_links.html中:

在本章中,我们涵盖了大量信息,到目前为止,您应该熟悉创建视图和模板的过程。我们学会了如何呈现Jinja2模板以及如何将数据从视图传递到模板上下文中。我们还学会了如何在模板中修改上下文数据,使用Jinja2标签和过滤器。在本章的后半部分,我们为网站设计了URL结构,并将其转换为Flask视图。我们为网站添加了一个简单的全文搜索功能,并通过为条目和标签列表添加分页链接来结束。

在下一章中,我们将学习如何通过网站使用表单创建和编辑博客条目。我们将学习如何处理和验证用户输入,然后将更改保存到数据库中。我们还将添加一个上传照片的功能,以便在博客条目中嵌入图像。

在本章中,我们将学习如何使用表单直接通过网站修改博客上的内容。这将是一个有趣的章节,因为我们将添加各种新的与网站交互的方式。我们将创建用于处理Entry模型的表单,学习如何接收和验证用户数据,并最终更新数据库中的值。表单处理和验证将由流行的WTForms库处理。我们将继续构建视图和模板来支持这些新的表单,并在此过程中学习一些新的Jinja2技巧。

在撰写本书时,WTForms2.0仍然是一个开发版本,但应该很快就会成为官方版本。因此,我们将在本书中使用版本2.0。

让我们开始通过将WTForms安装到我们的博客项目virtualenv中:

(blog)$pipinstall"wtforms>=2.0"SuccessfullyinstalledwtformsCleaningup...我们可以通过打开一个shell并检查项目版本来验证安装是否成功:

(blog)$./manage.pyshellIn[1]:importwtformsIn[2]:wtforms.__version__Out[2]:'2.0dev'我的版本显示了开发版本,因为2.0尚未正式发布。

我们的目标是能够直接通过我们的网站创建和编辑博客条目,因此我们需要回答的第一个问题是——我们将如何输入我们的新条目的数据?答案当然是使用表单。表单是HTML标准的一部分,它允许我们使用自由格式的文本输入、大型多行文本框、下拉选择、复选框、单选按钮等。当用户提交表单时,表单会指定一个URL来接收表单数据。然后该URL可以处理数据,然后以任何喜欢的方式做出响应。

对于博客条目,让我们保持简单,只有三个字段:

在entries目录中,创建一个名为forms.py的新Python文件。我们将定义一个简单的表单类,其中包含这些字段。打开forms.py并添加以下代码:

importwtformsfrommodelsimportEntryclassEntryForm(wtforms.Form):title=wtforms.StringField('Title')body=wtforms.TextAreaField('Body')status=wtforms.SelectField('Entrystatus',choices=((Entry.STATUS_PUBLIC,'Public'),(Entry.STATUS_DRAFT,'Draft')),coerce=int)这应该看起来与我们的模型定义非常相似。请注意,我们正在使用模型中列的名称作为表单字段的名称:这将允许WTForms自动在Entry模型字段和表单字段之间复制数据。

前两个字段,标题和正文,都指定了一个参数:在渲染表单时将显示的标签。状态字段包含一个标签以及两个额外的参数:choices和coerce。choices参数由一个2元组的列表组成,其中第一个值是我们感兴趣存储的实际值,第二个值是用户友好的表示。第二个参数,coerce,将把表单中的值转换为整数(默认情况下,它将被视为字符串,这是我们不想要的)。

为了开始使用这个表单,我们需要创建一个视图,该视图将显示表单并在提交时接受数据。为此,让我们打开entries蓝图模块,并定义一个新的URL路由来处理条目创建。在blueprint.py文件的顶部,我们需要从forms模块导入EntryForm类:

fromappimportdbfromhelpersimportobject_listfrommodelsimportEntry,Tagfromentries.formsimportEntryForm然后,在detail视图的定义之上,我们将添加一个名为create的新视图,该视图将通过导航到/entries/create/来访问。我们必须将其放在detail视图之上的原因是因为Flask将按照定义的顺序搜索URL路由。由于/entries/create/看起来非常像一个条目详细信息URL(想象条目的标题是create),如果首先定义了详细信息路由,Flask将在那里停止,永远不会到达创建路由。

在我们的创建视图中,我们将简单地实例化表单并将其传递到模板上下文中。添加以下视图定义:

@entries.route('/create/')defcreate():form=EntryForm()returnrender_template('entries/create.html',form=form)在我们添加代码将新条目保存到数据库之前,让我们构建一个模板,看看我们的表单是什么样子。然后我们将回过头来添加代码来验证表单数据并创建新条目。

让我们为我们的新表单构建一个基本模板。在其他条目模板旁边创建一个名为create.html的新模板。相对于应用程序目录,该文件的路径应为entries/templates/entries/create.html。我们将扩展基本模板并覆盖内容块以显示我们的表单。由于我们使用的是bootstrap,我们将使用特殊的CSS类来使我们的表单看起来漂亮。添加以下HTML代码:

尝试提交表单。当您点击创建按钮时,您应该会看到以下错误消息:

您看到此消息的原因是因为默认情况下,Flask视图只会响应HTTPGET请求。当我们提交表单时,浏览器会发送POST请求,而我们的视图目前不接受。让我们返回create视图并添加代码来正确处理POST请求。

每当表单对数据进行更改(创建、编辑或删除某些内容)时,该表单应指定POST方法。其他表单,例如我们的搜索表单,不进行任何更改,应使用GET方法。此外,当使用GET方法提交表单时,表单数据将作为查询字符串的一部分提交。

在修改视图之前,让我们向我们的EntryForm添加一个辅助方法,我们将使用该方法将数据从表单复制到我们的Entry对象中。打开forms.py并进行以下添加:

classEntryForm(wtforms.Form):...defsave_entry(self,entry):self.populate_obj(entry)entry.generate_slug()returnentry这个辅助方法将用表单数据填充我们传入的entry,根据标题重新生成条目的slug,然后返回entry对象。

现在表单已配置为填充我们的Entry模型,我们可以修改视图以接受和处理POST请求。我们将使用两个新的Flask辅助函数,因此修改blueprint.py顶部的导入,添加redirect和url_for:

fromflaskimportBlueprint,redirect,render_template,request,url_for添加导入后,更新blueprint.py中create视图的以下更改:

fromappimportdb@entries.route('/create/',methods=['GET','POST'])defcreate():ifrequest.method=='POST':form=EntryForm(request.form)ifform.validate():entry=form.save_entry(Entry())db.session.add(entry)db.session.commit()returnredirect(url_for('entries.detail',slug=entry.slug))else:form=EntryForm()returnrender_template('entries/create.html',form=form)这是相当多的新代码,让我们仔细看看发生了什么。首先,我们在路由装饰器中添加了一个参数,指示此视图接受GET和POST请求。这将消除当我们提交表单时出现的方法不允许错误。

在视图的主体中,我们现在正在检查request方法,并根据这一点做两件事中的一件。让我们首先看看'else'子句。当我们收到GET请求时,比如当有人打开他们的浏览器并导航到/entries/create/页面时,代码分支将执行。当这种情况发生时,我们只想显示包含表单的HTML页面,因此我们将实例化一个表单并将其传递到模板上下文中。

如果这是一个POST请求,当有人提交表单时会发生,我们想要实例化EntryForm并传入原始表单数据。Flask将原始的POST数据存储在特殊属性request.form中,这是一个类似字典的对象。WTForms知道如何解释原始表单数据并将其映射到我们定义的字段。

在用原始表单数据实例化我们的表单之后,我们需要检查并确保表单有效,通过调用form.validate()。如果表单由于某种原因未能验证,我们将简单地将无效的表单传递到上下文并呈现模板。稍后您将看到我们如何在用户的表单提交出现问题时向用户显示错误消息。

打开你的浏览器,试一试。

我们的表单存在一个明显的问题:现在没有任何东西可以阻止我们意外地提交一个空的博客条目。为了确保在保存时有标题和内容,我们需要使用一个名为验证器的WTForm对象。验证器是应用于表单数据的规则,WTForms附带了许多有用的验证器。一些常用的验证器列在下面:

对于博客条目表单,我们将只使用DataRequired验证器来确保条目不能在没有标题或正文内容的情况下创建。让我们打开forms.py并将验证器添加到我们的表单定义中。总的来说,我们的表单模块应该如下所示:

importwtformsfromwtforms.validatorsimportDataRequiredfrommodelsimportEntryclassEntryForm(wtforms.Form):title=wtforms.StringField('Title',validators=[DataRequired()])body=wtforms.TextAreaField('Body',validators=[DataRequired()])status=wtforms.SelectField('Entrystatus',choices=((Entry.STATUS_PUBLIC,'Public'),(Entry.STATUS_DRAFT,'Draft')),coerce=int)defsave_entry(self,entry):self.populate_obj(entry)entry.generate_slug()returnentry启动开发服务器,现在尝试提交一个空表单。正如你所期望的那样,由于对form.validate()的调用返回False,它将无法保存。不幸的是,前端没有任何指示我们的表单为什么没有保存。幸运的是,WTForms将使验证错误在模板中可用,我们所需要做的就是修改我们的模板来显示它们。

为了显示验证错误,我们将使用几个bootstrapCSS类和结构,但最终结果将非常好看,如下面的截图所示:

对create.html模板中的字段显示代码进行以下更改:

{%forfieldinform%}{{field.label(class='col-sm-3control-label')}}{{field(class='form-control')}}{%iffield.errors%}{%endif%}{%forerrorinfield.errors%}{{error}}{%endfor%}

{%endfor%}我们通过查看field.errors属性来检查字段是否有任何错误。如果有任何错误,那么我们会做以下事情:

现在,您可以使用表单创建有效的博客条目,该表单还会执行一些验证,以确保您不会提交空白表单。在下一节中,我们将描述如何重复使用相同的表单来编辑现有条目。

信不信由你,我们实际上可以使用相同的表单来编辑现有条目。我们只需要对视图和模板逻辑进行一些微小的更改,所以让我们开始吧。

为了编辑条目,我们将需要一个视图,因此我们将需要一个URL。因为视图需要知道我们正在编辑哪个条目,所以将其作为URL结构的一部分传达是很重要的,因此我们将在/entries//edit/设置edit视图。打开entries/blueprint.py,在详细视图下方,添加以下代码以获取edit视图。请注意与create视图的相似之处:

@entries.route('//edit/',methods=['GET','POST'])defedit(slug):entry=Entry.query.filter(Entry.slug==slug).first_or_404()ifrequest.method=='POST':form=EntryForm(request.form,obj=entry)ifform.validate():entry=form.save_entry(entry)db.session.add(entry)db.session.commit()returnredirect(url_for('entries.detail',slug=entry.slug))else:form=EntryForm(obj=entry)returnrender_template('entries/edit.html',entry=entry,form=form)就像我们在create视图中所做的那样,我们检查request方法,并根据它,我们将验证和处理表单,或者只是实例化它并将其传递给模板。

最大的区别在于我们如何实例化EntryForm。我们向它传递了一个额外的参数,obj=entry。当WTForms接收到一个obj参数时,它将尝试使用从obj中获取的值(在本例中是我们的博客条目)预填充表单字段。

我们还将在模板上下文中传递一个额外的值,即我们正在编辑的条目。我们这样做是为了能够向用户显示条目的标题;这样,我们可以使表单的取消按钮链接回条目详细视图。

正如您可能猜到的,edit.html模板几乎与create.html相同。由于字段渲染逻辑的复杂性,复制并粘贴所有代码似乎是一个坏主意。如果我们决定更改表单字段的显示方式,我们将发现自己需要修改多个文件,这应该始终是一个很大的警告信号。

为了避免这种情况,我们将使用一个强大的Jinja2功能,称为宏,来渲染我们的字段。字段渲染代码将在宏中定义,然后,无论我们想要渲染一个字段的地方,我们只需调用我们的宏。这样可以很容易地更改我们的字段样式。

由于这个宏对于我们可能希望显示的任何表单字段都是有用的,我们将把它放在我们应用程序的模板目录中。在应用程序的模板目录中,创建一个名为macros的新目录,并添加一个字段form_field.html。相对于应用程序目录,该文件的路径是templates/macros/form_field.html。添加以下代码:

{%macroform_field(field)%}{{field.label(class='col-sm-3control-label')}}{{field(class='form-control',**kwargs)}}{%iffield.errors%}{%endif%}{%forerrorinfield.errors%}{{error}}{%endfor%}

{%endmacro%}在大部分情况下,我们只是从create模板中复制并粘贴了字段渲染代码,但有一些区别我想指出:

现在让我们更新create.html以使用新的宏。为了使用这个宏,我们必须首先import它。然后我们可以用一个简单的宏调用替换所有的字段标记。通过这些更改,create.html模板应该是这样的:

在create.html和edit.html模板旁边创建一个名为delete.html的模板,并添加以下HTML:

当表单提交时,我们可以简单地从数据库中删除条目,但根据我的经验,我通常会后悔永久删除内容。我们不会真正从数据库中删除条目,而是给它一个_DELETED状态;我们将把它的状态改为STATUS_DELETED。然后我们将修改我们的视图,以便具有这种状态的条目永远不会出现在网站的任何部分。在所有方面,条目都消失了,但是,如果我们将来需要它,我们可以从数据库中检索它。在edit视图下面添加以下视图代码:

@entries.route('//delete/',methods=['GET','POST'])defdelete(slug):entry=Entry.query.filter(Entry.slug==slug).first_or_404()ifrequest.method=='POST':entry.status=Entry.STATUS_DELETEDdb.session.add(entry)db.session.commit()returnredirect(url_for('entries.index'))returnrender_template('entries/delete.html',entry=entry)我们还需要在model.py中的Entries模型中添加STATUS_DELETED:

首先,让我们更新entry_list辅助函数,以筛选出公共或草稿条目。

defentry_list(template,query,**context):valid_statuses=(Entry.STATUS_PUBLIC,Entry.STATUS_DRAFT)query=query.filter(Entry.status.in_(valid_statuses))ifrequest.args.get('q'):search=request.args['q']query=query.filter((Entry.body.contains(search))|(Entry.title.contains(search)))returnobject_list(template,query,**context)现在我们可以确信,无论我们在哪里显示条目列表,都不会显示已删除的条目。

现在让我们添加一个新的辅助函数来通过其slug检索Entry。如果找不到条目,我们将返回404。在entry_list下面添加以下代码:

defget_entry_or_404(slug):valid_statuses=(Entry.STATUS_PUBLIC,Entry.STATUS_DRAFT)(Entry.query.filter((Entry.slug==slug)&(Entry.status.in_(valid_statuses))).first_or_404())用get_entry_or_404替换detail、edit和delete视图中的Entry.query.filter()调用。以下是更新后的detail视图:

@entries.route('//')defdetail(slug):entry=get_entry_or_404(slug)returnrender_template('entries/detail.html',entry=entry)使用闪存消息当用户在网站上执行操作时,通常会在随后的页面加载时显示一次性消息,指示他们的操作已成功。这些称为闪存消息,Flask带有一个辅助函数来显示它们。为了开始使用闪存消息,我们需要在config模块中添加一个秘钥。秘钥是必要的,因为闪存消息存储在会话中,而会话又存储为加密的cookie。为了安全地加密这些数据,Flask需要一个秘钥。

打开config.py并添加一个秘钥。可以是短语、随机字符,任何你喜欢的东西:

classConfiguration(object):APPLICATION_DIR=current_directoryDEBUG=TrueSECRET_KEY='flaskisfun!'#Createauniquekeyforyourapp.SQLALCHEMY_DATABASE_URI='sqlite:///%s/blog.db'%APPLICATION_DIR现在,无论我们的用户在哪个页面上执行操作,我们都希望向他们显示一个消息,指示他们的操作成功。这意味着我们将在create,edit和delete视图中添加一个消息。打开条目蓝图并将闪存函数添加到模块顶部的flask导入列表中:

fromflaskimportBlueprint,flash,redirect,render_template,request,url_for然后,在每个适当的视图中,让我们调用flash并显示一个有用的消息。在重定向之前应该发生调用:

defcreate():...db.session.commit()flash('Entry"%s"createdsuccessfully.'%entry.title,'success')returnredirect(url_for('entries.detail',slug=entry.slug))...defedit(slug):...db.session.commit()flash('Entry"%s"hasbeensaved.'%entry.title,'success')returnredirect(url_for('entries.detail',slug=entry.slug))...defdelete(slug):...db.session.commit()flash('Entry"%s"hasbeendeleted.'%entry.title,'success')returnredirect(url_for('entries.index'))...在模板中显示闪存消息因为我们并不总是知道在需要显示闪存消息时我们将在哪个页面上,所以将显示逻辑添加到基本模板是一种标准做法。Flask提供了一个Jinja2函数get_flashed_messages,它将返回一个待显示的消息列表。

打开base.html并添加以下代码。我已经将我的代码放在content_title块和content块之间:

{%blockcontent_title%}{%endblock%}

{%forcategory,messageinget_flashed_messages(with_categories=true)%}×{{message}}{%endfor%}{%blockcontent%}{%endblock%}让我们试试看!启动开发服务器并尝试添加一个新条目。保存后,您应该被重定向到新条目,并看到一个有用的消息,如下面的屏幕截图所示:

我们已经讨论了如何保存和修改条目上的标签。管理标签的最常见方法之一是使用逗号分隔的文本输入,因此我们可以将标签列为Python,Flask,Web-development。使用WTForms似乎非常简单,因为我们只需使用StringField。然而,由于我们正在处理数据库关系,这意味着我们需要在Tag模型和逗号分隔的字符串之间进行一些处理。

虽然我们可以通过许多方式来实现这一点,但我们将实现一个自定义字段类TagField,它将封装在逗号分隔的标签名称和Tag模型实例之间进行转换的所有逻辑。

另一个选项是在Entry模型上创建一个property。属性看起来像一个普通的对象属性,但实际上是getter和(有时)setter方法的组合。由于WTForms可以自动处理我们的模型属性,这意味着,如果我们在getter和setter中实现我们的转换逻辑,WTForms将正常工作。

让我们首先定义我们的标签字段类。我们需要重写两个重要的方法:

以下是TagField的实现。请注意,我们在处理用户输入时要特别小心,以避免在Tag表中创建重复行。我们还使用Python的set()数据类型来消除用户输入中可能的重复项。将以下类添加到forms.py中的EntryForm上方:

frommodelsimportTagclassTagField(wtforms.StringField):def_value(self):ifself.data:#Displaytagsasacomma-separatedlist.return','.join([tag.namefortaginself.data])return''defget_tags_from_string(self,tag_string):raw_tags=tag_string.split(',')#Filteroutanyemptytagnames.tag_names=[name.strip()fornameinraw_tagsifname.strip()]#Querythedatabaseandretrieveanytagswehavealreadysaved.existing_tags=Tag.query.filter(Tag.name.in_(tag_names))#Determinewhichtagnamesarenew.new_names=set(tag_names)-set([tag.namefortaginexisting_tags])#CreatealistofunsavedTaginstancesforthenewtags.new_tags=[Tag(name=name)fornameinnew_names]#Returnalltheexistingtags+allthenew,unsavedtags.returnlist(existing_tags)+new_tagsdefprocess_formdata(self,valuelist):ifvaluelist:self.data=self.get_tags_from_string(valuelist[0])else:self.data=[]现在,我们只需要将字段添加到EntryForm中。在status字段下面添加以下字段。请注意description关键字参数的使用:

classEntryForm(wtforms.Form):...tags=TagField('Tags',description='Separatemultipletagswithcommas.')为了显示这个有用的description文本,让我们对form_field宏进行快速修改:

{%macroform_field(field)%}{{field.label(class='col-sm-3control-label')}}{{field(class='form-control',**kwargs)}}{%iffield.errors%}{%endif%}{%iffield.description%}{{field.description|safe}}{%endif%}{%forerrorinfield.errors%}{{error}}{%endfor%}{%endmacro%}启动开发服务器,并尝试保存一些标签。您的表单应该看起来像下面的屏幕截图:

我们将通过为网站添加一个图片上传功能来完成表单处理章节。这个功能将是一个简单的视图,接受一个图像文件并将其存储在服务器上的上传目录中。这将使我们能够轻松在博客条目中显示图像。

第一步是创建一个处理图像上传的表单。除了EntryForm,让我们添加一个名为ImageForm的新表单。这个表单将非常简单,包含一个文件输入。我们将使用自定义验证器来确保上传的文件是有效的图像。将以下代码添加到forms.py中:

classImageForm(wtforms.Form):file=wtforms.FileField('Imagefile')在我们添加一个视图来保存表单之前,我们需要知道我们将在哪里保存文件。通常,应用程序的资源(如图像、JavaScript和样式表)都是从一个名为static的单个目录中提供的。通常的做法是在web服务器中覆盖此目录的路径,以便它可以在不经过Python中介的情况下传输此文件,从而使访问速度更快。我们利用static目录来存储我们的图像上传。在博客项目的app目录中,让我们创建一个名为static的新目录和一个子目录images:

(blog)$cd~/projects/blog/blog/app(blog)$mkdir-pstatic/images现在让我们向配置文件中添加一个新值,这样我们就可以轻松地引用磁盘上图像的路径。这样可以简化我们的代码,以后如果我们选择更改此位置,也会更加方便。打开config.py并添加以下值:

classConfiguration(object):...STATIC_DIR=os.path.join(APPLICATION_DIR,'static')IMAGES_DIR=os.path.join(STATIC_DIR,'images')处理文件上传我们现在准备创建一个用于处理图像上传的视图。逻辑将与我们的其他表单处理视图非常相似,唯一的区别是,在验证表单后,我们将把上传的文件保存到磁盘上。由于这些图像是用于我们博客条目的,我将视图添加到entriesblueprint中,可在/entries/image-upload/访问。

我们需要导入我们的新表单以及其他辅助工具。打开blueprint.py并在模块顶部添加以下导入:

importosfromflaskimportBlueprint,flash,redirect,render_template,request,url_forfromwerkzeugimportsecure_filenamefromappimportapp,dbfromhelpersimportobject_listfrommodelsimportEntry,Tagfromentries.formsimportEntryForm,ImageForm在视图列表的顶部,让我们添加新的image-upload视图。重要的是它出现在detail视图之前,否则Flask会错误地将/image-upload/视为博客条目的slug。添加以下视图定义:

@entries.route('/image-upload/',methods=['GET','POST'])defimage_upload():ifrequest.method=='POST':form=ImageForm(request.form)ifform.validate():image_file=request.files['file']filename=os.path.join(app.config['IMAGES_DIR'],secure_filename(image_file.filename))image_file.save(filename)flash('Saved%s'%os.path.basename(filename),'success')returnredirect(url_for('entries.index'))else:form=ImageForm()returnrender_template('entries/image_upload.html',form=form)这里的大部分代码可能看起来很熟悉,值得注意的例外是使用request.files和secure_filename。当文件上传时,Flask会将其存储在request.files中,这是一个特殊的字典,以表单字段的名称为键。我们使用secure_filename进行一些路径连接,以防止恶意文件名,并生成到static/images目录的正确路径,然后将上传的文件保存到磁盘上。就是这么简单。

让我们为我们的图片上传表单创建一个简单的模板。在entries模板目录中创建一个名为image_upload.html的文件,并添加以下代码:

Flask将自动从我们的/static/目录中提供文件。当我们在第十章部署我们的网站时,部署您的应用程序,我们将使用Nginxweb服务器来提供静态资产,但是对于本地开发,Flask使事情变得非常简单。

(blog)$cdstatic/&&find.-typef./fonts/glyphicons-halflings-regular.woff./fonts/glyphicons-halflings-regular.ttf./fonts/glyphicons-halflings-regular.eot./fonts/glyphicons-halflings-regular.svg./images/2012-07-17_16.18.18.jpg./js/jquery-1.10.2.min.js./js/bootstrap.min.js./css/bootstrap.min.css为了将我们的基本模板指向这些文件的本地版本,我们将使用url_for助手来生成正确的URL。打开base.html,删除旧的样式表和JavaScript标签,并用本地版本替换它们:

在本章中,我们添加了各种与网站交互的新方法。现在可以直接通过网站创建和修改内容。我们讨论了如何使用WTForms创建面向对象的表单,包括从视图处理和验证表单数据,以及将表单数据写入数据库。我们还创建了模板来显示表单和验证错误,并使用Jinja2宏来删除重复的代码,使代码更加模块化。然后,我们能够向用户显示单次使用的闪存消息,当他们执行操作时。最后,我们还解释了如何使用WTForms和Flask处理文件上传,并提供静态资产,如JavaScript、样式表和图像上传。

在本章中,我们将向我们的网站添加用户身份验证。能够区分一个用户和另一个用户使我们能够开发一整套新功能。例如,我们将看到如何限制对创建、编辑和删除视图的访问,防止匿名用户篡改网站内容。我们还可以向用户显示他们的草稿帖子,但对其他人隐藏。本章将涵盖向网站添加身份验证层的实际方面,并以讨论如何使用会话跟踪匿名用户结束。

如果您认为还有其他字段可能有用,请随意向此列表添加自己的内容。

现在我们有了字段列表,让我们创建model类。打开models.py,在Tag模型下面,添加以下代码:

classUser(db.Model):id=db.Column(db.Integer,primary_key=True)email=db.Column(db.String(64),unique=True)password_hash=db.Column(db.String(255))name=db.Column(db.String(64))slug=db.Column(db.String(64),unique=True)active=db.Column(db.Boolean,default=True)created_timestamp=db.Column(db.DateTime,default=datetime.datetime.now)def__init__(self,*args,**kwargs):super(User,self).__init__(*args,**kwargs)self.generate_slug()defgenerate_slug(self):ifself.name:self.slug=slugify(self.name)正如您在第二章中所记得的,使用SQLAlchemy的关系数据库,我们需要创建一个迁移,以便将这个表添加到我们的数据库中。从命令行,我们将使用manage.py助手来审查我们的模型并生成迁移脚本:

(blog)$pythonmanage.pydbmigrateINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.autogenerate.compare]Detectedaddedtable'user'Generating/home/charles/projects/blog/app/migrations/versions/40ce2670e7e2_.py...done生成迁移后,我们现在可以运行dbupgrade来进行模式更改:

另一方面,Flask-Login不会做以下事情:

让我们开始吧。使用pip安装Flask-Login:

将以下代码添加到app.py。导入放在模块的顶部,其余部分放在末尾:

打开models.py并添加以下代码行:

fromappimportlogin_manager@login_manager.user_loaderdef_user_loader(user_id):returnUser.query.get(int(user_id))现在Flask-Login知道如何将用户ID转换为User对象,并且该用户将作为g.user对我们可用。

打开models.py并向User类添加以下方法:

现在我们已经配置了Flask-Login,让我们添加一些代码,以便我们可以创建一些用户。

创建新用户就像创建条目或标签一样,只有一个例外:我们需要安全地对用户的密码进行哈希处理。您永远不应该以明文形式存储密码,并且由于黑客的技术日益复杂,最好使用强大的加密哈希函数。我们将使用Flask-Bcrypt扩展来对我们的密码进行哈希处理和检查,因此让我们使用pip安装这个扩展:

(blog)$pipinstallflask-bcrypt...SuccessfullyinstalledFlask-BcryptCleaningup...打开app.py并添加以下代码来注册扩展到我们的应用程序:

fromflask.ext.bcryptimportBcryptbcrypt=Bcrypt(app)现在让我们为User对象添加一些方法,以便创建和检查密码变得简单:

fromappimportbcryptclassUser(db.Model):#...columndefinitions,othermethods...@staticmethoddefmake_password(plaintext):returnbcrypt.generate_password_hash(plaintext)defcheck_password(self,raw_password):returnbcrypt.check_password_hash(self.password_hash,raw_password)@classmethoddefcreate(cls,email,password,**kwargs):returnUser(email=email,password_hash=User.make_password(password),**kwargs)@staticmethoddefauthenticate(email,password):user=User.query.filter(User.email==email).first()ifuseranduser.check_password(password):returnuserreturnFalsemake_password方法接受明文密码并返回哈希版本,而check_password方法接受明文密码并确定它是否与数据库中存储的哈希版本匹配。然而,我们不会直接使用这些方法。相反,我们将创建两个更高级的方法,create和authenticate。create方法将创建一个新用户,在保存之前自动对密码进行哈希处理,而authenticate方法将根据用户名和密码检索用户。

通过创建一个新用户来尝试这些方法。打开一个shell,并使用以下代码作为示例,为自己创建一个用户:

importwtformsfromwtformsimportvalidatorsfrommodelsimportUserclassLoginForm(wtforms.Form):email=wtforms.StringField("Email",validators=[validators.DataRequired()])password=wtforms.PasswordField("Password",validators=[validators.DataRequired()])remember_me=wtforms.BooleanField("Rememberme",default=True)提示请注意,WTForms还提供了一个电子邮件验证器。但是,正如该验证器的文档所告诉我们的那样,它非常原始,可能无法捕获所有边缘情况,因为完整的电子邮件验证实际上是非常困难的。

为了在正常的WTForms验证过程中验证用户的凭据,我们将重写表单的validate()方法。如果找不到电子邮件或密码不匹配,我们将在电子邮件字段下方显示错误。将以下方法添加到LoginForm类:

在app目录中打开views.py并添加以下代码:

fromflaskimportflash,redirect,render_template,request,url_forfromflask.ext.loginimportlogin_userfromappimportappfromappimportlogin_managerfromformsimportLoginForm@app.route("/")defhomepage():returnrender_template("homepage.html")@app.route("/login/",methods=["GET","POST"])deflogin():ifrequest.method=="POST":form=LoginForm(request.form)ifform.validate():login_user(form.user,remember=form.remember_me.data)flash("Successfullyloggedinas%s."%form.user.email,"success")returnredirect(request.args.get("next")orurl_for("homepage"))else:form=LoginForm()returnrender_template("login.html",form=form)魔法发生在我们成功验证表单(因此验证了用户身份)后的POST上。我们调用login_user,这是Flask-Login提供的一个辅助函数,用于设置正确的会话值。然后我们设置一个闪存消息并将用户送上路。

最后让我们添加一个视图,用于将用户从网站中注销。有趣的是,此视图不需要模板,因为用户将简单地通过视图,在其会话注销后被重定向。将以下import语句和注销视图代码添加到views.py:

#Modifytheimportatthetopofthemodule.fromflask.ext.loginimportlogin_user,logout_user#Addlogout_user@app.route("/logout/")deflogout():logout_user()flash('Youhavebeenloggedout.','success')returnredirect(request.args.get('next')orurl_for('homepage'))再次说明,我们接受nextURL作为查询字符串的一部分,默认为主页,如果未指定URL。

正如您可能还记得本章早些时候所说的,我们添加了一个信号处理程序,将当前用户存储为Flaskg对象的属性。我们可以在模板中访问这个对象,所以我们只需要在模板中检查g.user是否已经通过身份验证。

打开base.html并对导航栏进行以下添加:

目前,我们所有的博客视图都是不受保护的,任何人都可以访问它们。为了防止恶意用户破坏我们的条目,让我们为实际修改数据的视图添加一些保护。Flask-Login提供了一个特殊的装饰器login_required,我们将使用它来保护应该需要经过身份验证的视图。

让我们浏览条目蓝图并保护所有修改数据的视图。首先在blueprint.py模块的顶部添加以下导入:

fromflask.ext.loginimportlogin_requiredlogin_required是一个装饰器,就像app.route一样,所以我们只需包装我们希望保护的视图。例如,这是如何保护image_upload视图的方法:

@entries.route('/image-upload/',methods=['GET','POST'])@login_requireddefimage_upload():...浏览模块,并在以下视图中添加login_required装饰器,注意要在路由装饰器下面添加:

当匿名用户尝试访问这些视图时,他们将被重定向到login视图。作为额外的奖励,Flask-Login将在重定向到login视图时自动处理指定下一个参数,因此用户将返回到他们试图访问的页面。

正如您可能还记得我们在第一章中创建的规范,创建您的第一个Flask应用程序,我们的博客网站将支持多个作者。当创建条目时,我们将把当前用户存储在条目的作者列中。为了存储编写给定Entry的User,我们将在用户和条目之间创建一个一对多的关系,以便一个用户可以有多个条目:

为了创建一对多的关系,我们将在Entry模型中添加一个指向User表中用户的列。这个列将被命名为author_id,因为它引用了一个User,我们将把它设为外键。打开models.py并对Entry模型进行以下修改:

classEntry(db.Model):modified_timestamp=...author_id=db.Column(db.Integer,db.ForeignKey("user.id"))tags=...由于我们添加了一个新的列,我们需要再次创建一个迁移。从命令行运行dbmigrate和dbupgrade:

(blog)$pythonmanage.pydbmigrateINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.autogenerate.compare]Detectedaddedcolumn'entry.author_id'Generating/home/charles/projects/blog/app/migrations/versions/33011181124e_.py...done(blog)$pythonmanage.pydbupgradeINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.migration]Runningupgrade40ce2670e7e2->33011181124e,emptymessage就像我们对标签所做的那样,最后一步将是在用户模型上创建一个反向引用,这将允许我们访问特定用户关联的Entry行。因为用户可能有很多条目,我们希望对其执行额外的过滤操作,我们将把反向引用暴露为一个查询,就像我们为标签条目所做的那样。

在User类中,在created_timestamp列下面添加以下代码行:

entries=db.relationship('Entry',backref='author',lazy='dynamic')现在我们有能力将User作为博客条目的作者存储起来,下一步将是在创建条目时填充这个列。

如果数据库中有任何博客条目,我们还需要确保它们被分配给一个作者。从交互式shell中,让我们手动更新所有现有条目上的作者字段:

In[8]:Entry.query.update({"author_id":user.id})Out[8]:6这个查询将返回更新的行数,在这种情况下是数据库中的条目数。要保存这些更改,再次调用commit():

因为我们正在使用g对象来访问用户,所以我们需要导入它,所以在条目蓝图的顶部添加以下导入语句:

fromflaskimportg在条目蓝图中,我们现在需要修改Entry对象的实例化,手动设置作者属性。对create视图进行以下更改:

ifform.validate():entry=form.save_entry(Entry(author=g.user))db.session.add(entry)当您要创建一个条目时,您现在将被保存在数据库中作为该条目的作者。试一试吧。

为了清晰地实现此保护,我们将再次重构条目蓝图中的辅助函数。对条目蓝图进行以下修改:

defget_entry_or_404(slug,author=None):query=Entry.query.filter(Entry.slug==slug)ifauthor:query=query.filter(Entry.author==author)else:query=filter_status_by_user(query)returnquery.first_or_404()我们引入了一个新的辅助函数filter_status_by_user。此函数将确保匿名用户无法看到草稿条目。在get_entry_or_404下方的条目蓝图中添加以下函数:

deffilter_status_by_user(query):ifnotg.user.is_authenticated:returnquery.filter(Entry.status==Entry.STATUS_PUBLIC)else:returnquery.filter(Entry.status.in_((Entry.STATUS_PUBLIC,Entry.STATUS_DRAFT)))为了限制对edit和delete视图的访问,我们现在只需要将当前用户作为作者参数传递。对编辑和删除视图进行以下修改:

entry=get_entry_or_404(slug,author=None)如果您尝试访问您未创建的条目的edit或delete视图,您将收到404响应。

最后,让我们修改条目详细模板,以便除了条目的作者之外,所有用户都无法看到编辑和删除链接。在您的entries应用程序中编辑模板entries/detail.html,您的代码可能如下所示:

deffilter_status_by_user(query):ifnotg.user.is_authenticated:query=query.filter(Entry.status==Entry.STATUS_PUBLIC)else:#Allowusertoviewtheirowndrafts.query=query.filter((Entry.status==Entry.STATUS_PUBLIC)|((Entry.author==g.user)&(Entry.status!=Entry.STATUS_DELETED)))returnquery新的查询可以解析为:“给我所有公共条目,或者我是作者的未删除条目。”

由于get_entry_or_404已经使用了filter_status_by_user辅助函数,因此detail、edit和delete视图已经准备就绪。我们只需要处理使用entry_list辅助函数的各种列表视图。让我们更新entry_list辅助函数以使用新的filter_status_by_user辅助函数:

query=filter_status_by_user(query)valid_statuses=(Entry.STATUS_PUBLIC,Entry.STATUS_DRAFT)query=query.filter(Entry.status.in_(valid_statuses))ifrequest.args.get("q"):search=request.args["q"]query=query.filter((Entry.body.contains(search))|(Entry.title.contains(search)))returnobject_list(template,query,**context)就是这样!我希望这展示了一些辅助函数在正确的位置上是如何真正简化开发者生活的。在继续进行最后一节之前,我建议创建一个或两个用户,并尝试新功能。

fromflaskimportrequest,session@app.before_requestdef_last_page_visited():if"current_page"insession:session["last_page"]=session["current_page"]session["current_page"]=request.path默认情况下,Flask会话只持续到浏览器关闭。如果您希望会话持久存在,即使在重新启动之间也是如此,只需设置session.permanent=True。

与g对象一样,session对象可以直接从模板中访问。

作为练习,尝试为您的网站实现一个简单的主题选择器。创建一个视图,允许用户选择颜色主题,并将其存储在会话中。然后,在模板中,根据用户选择的主题应用额外的CSS规则。

在下一章中,我们将构建一个管理仪表板,允许超级用户执行诸如创建新用户和修改站点内容等操作。我们还将收集和显示各种站点指标,如页面浏览量,以帮助可视化哪些内容驱动了最多的流量。

在本章中,我们将为我们的网站构建一个管理仪表板。我们的管理仪表板将使特定的、选择的用户能够管理整个网站上的所有内容。实质上,管理站点将是数据库的图形前端,支持在应用程序表中创建、编辑和删除行的操作。优秀的Flask-Admin扩展几乎提供了所有这些功能,但我们将超越默认值,扩展和定制管理页面。

Flask-Admin为Flask应用程序提供了一个现成的管理界面。Flask-Admin还与SQLAlchemy很好地集成,以提供用于管理应用程序模型的视图。

下面的图像是对本章结束时Entry管理员将会是什么样子的一个sneakpreview:

虽然这种功能需要相对较少的代码,但我们仍然有很多内容要涵盖,所以让我们开始吧。首先使用pip将Flask-Admin安装到virtualenv中。在撰写本文时,Flask-Admin的当前版本是1.0.7。

(blog)$pipinstallFlask-AdminDownloading/unpackingFlask-Admin...SuccessfullyinstalledFlask-AdminCleaningup...如果您希望测试它是否安装正确,可以输入以下代码:

(blog)$pythonmanage.pyshellIn[1]:fromflask.extimportadminIn[2]:printadmin.__version__1.0.7将Flask-Admin添加到我们的应用程序与我们应用程序中的其他扩展不同,我们将在其自己的模块中设置管理扩展。我们将编写几个特定于管理的类,因此将它们放在自己的模块中是有意义的。在app目录中创建一个名为admin.py的新模块,并添加以下代码:

fromflask.ext.adminimportAdminfromappimportappadmin=Admin(app,'BlogAdmin')因为我们的admin模块依赖于app模块,为了避免循环导入,我们需要确保在app之后加载admin。打开main.py模块并添加以下内容:

fromflaskimportrequest,sessionfromappimportapp,dbimportadmin#Thislineisnew,placedaftertheappimport.importmodelsimportviews现在,您应该能够启动开发服务器并导航到/admin/以查看一个简单的管理员仪表板-默认的仪表板,如下图所示:

随着您在本章中的进展,我们将把这个无聊和普通的管理界面变成一个丰富而强大的仪表板,用于管理您的博客。

Flask-Admin带有一个contrib包,其中包含专门设计用于与SQLAlchemy模型一起工作的特殊视图类。这些类提供开箱即用的创建、读取、更新和删除功能。

打开admin.py并更新以下代码:

fromflask.ext.adminimportAdminfromflask.ext.admin.contrib.sqlaimportModelViewfromappimportapp,dbfrommodelsimportEntry,Tag,Useradmin=Admin(app,'BlogAdmin')admin.add_view(ModelView(Entry,db.session))admin.add_view(ModelView(Tag,db.session))admin.add_view(ModelView(User,db.session))请注意我们如何调用admin.add_view()并传递ModelView类的实例,以及db会话,以便它可以访问数据库。Flask-Admin通过提供一个中央端点来工作,我们开发人员可以向其中添加我们自己的视图。

启动开发服务器并尝试再次打开您的管理站点。它应该看起来像下面的截图:

尝试通过在导航栏中选择其链接来点击我们模型的视图之一。点击Entry链接以干净的表格格式显示数据库中的所有条目。甚至有链接可以创建、编辑或删除条目,如下一个截图所示:

所有看起来都像下面的截图:

如前面的截图所示,Flask-Admin在处理我们的外键到键和多对多字段(作者和标签)方面做得非常出色。它还相当不错地选择了要为给定字段使用哪个HTML小部件,如下所示:

不幸的是,这个表单存在一些明显的问题,如下所示:

在接下来的部分中,我们将看到如何自定义Admin类和ModelView类,以便管理员真正为我们的应用程序工作。

让我们暂时把表单放在一边,专注于清理列表。为此,我们将创建一个Flask-Admin的子类ModelView。ModelView类提供了许多扩展点和属性,用于控制列表显示的外观和感觉。

我们将首先通过手动指定我们希望显示的属性来清理列表列。此外,由于我们将在单独的列中显示作者,我们将要求Flask-Admin从数据库中高效地获取它。打开admin.py并更新以下代码:

fromflask.ext.adminimportAdminfromflask.ext.admin.contrib.sqlaimportModelViewfromappimportapp,dbfrommodelsimportEntry,Tag,UserclassEntryModelView(ModelView):column_list=['title','status','author','tease','tag_list','created_timestamp',]column_select_related_list=['author']#EfficientlySELECTtheauthor.admin=Admin(app,'BlogAdmin')admin.add_view(EntryModelView(Entry,db.session))admin.add_view(ModelView(Tag,db.session))admin.add_view(ModelView(User,db.session))您可能会注意到tease和tag_list实际上不是我们Entry模型中的列名。Flask-Admin允许您使用任何属性作为列值。我们还指定要用于创建对其他模型的引用的列。打开models.py模块,并向Entry模型添加以下属性:

@propertydeftag_list(self):return','.join(tag.namefortaginself.tags)@propertydeftease(self):returnself.body[:100]现在,当您访问Entry管理员时,您应该看到一个干净、可读的表格,如下图所示:

让我们也修复状态列的显示。这些数字很难记住-最好显示人类可读的值。Flask-Admin带有枚举字段(如状态)的辅助程序。我们只需要提供要显示值的状态值的映射,Flask-Admin就会完成剩下的工作。在EntryModelView中进行以下添加:

classEntryModelView(ModelView):_status_choices=[(choice,label)forchoice,labelin[(Entry.STATUS_PUBLIC,'Public'),(Entry.STATUS_DRAFT,'Draft'),(Entry.STATUS_DELETED,'Deleted'),]]column_choices={'status':_status_choices,}column_list=['title','status','author','tease','tag_list','created_timestamp',]column_select_related_list=['author']我们的Entry列表视图看起来好多了。现在让我们对User列表视图进行一些改进。同样,我们将对ModelView进行子类化,并指定要覆盖的属性。在admin.py中在EntryModelView下面添加以下类:

classUserModelView(ModelView):column_list=['email','name','active','created_timestamp']#BesuretousetheUserModelViewclasswhenregisteringtheUser:admin.add_view(UserModelView(User,db.session))以下截图显示了我们对User列表视图的更改:

除了显示我们的模型实例列表外,Flask-Admin还具有强大的搜索和过滤功能。假设我们有大量条目,并且想要找到包含特定关键字(如Python)的条目。如果我们能够在列表视图中输入我们的搜索,并且Flask-Admin只列出标题或正文中包含单词'Python'的条目,那将是有益的。

正如您所期望的那样,这是非常容易实现的。打开admin.py并添加以下行:

classEntryModelView(ModelView):_status_choices=[(choice,label)forchoice,labelin[(Entry.STATUS_PUBLIC,'Public'),(Entry.STATUS_DRAFT,'Draft'),(Entry.STATUS_DELETED,'Deleted'),]]column_choices={'status':_status_choices,}column_list=['title','status','author','tease','tag_list','created_timestamp',]column_searchable_list=['title','body']column_select_related_list=['author']当您重新加载Entry列表视图时,您将看到一个新的文本框,允许您搜索title和body字段,如下面的屏幕截图所示:

让我们通过向Entry列表添加几个过滤器来看看过滤器是如何工作的。我们将再次修改EntryModelView如下:

此时,Entry列表视图非常实用。作为练习,为UserModelView设置column_filters和column_searchable_list属性。

我们将通过展示如何自定义表单类来结束模型视图的讨论。您会记得,默认表单由Flask-Admin提供的有一些限制。在本节中,我们将展示如何自定义用于创建和编辑模型实例的表单字段的显示。

我们的目标是删除多余的字段,并为状态字段使用更合适的小部件,实现以下屏幕截图中所示的效果:

为了实现这一点,我们首先手动指定我们希望在表单上显示的字段列表。这是通过在EntryModelView类上指定form_columns属性来完成的:

classEntryModelView(ModelView):...form_columns=['title','body','status','author','tags']此外,我们希望status字段成为一个下拉小部件,使用各种状态的可读标签。由于我们已经定义了状态选择,我们将指示Flask-Admin使用WTFormsSelectField覆盖status字段,并传入有效选择的列表:

当包含外键的表单呈现到非常大的表时,Flask-Admin允许我们使用Ajax来获取所需的行。将以下属性添加到EntryModelView,现在您的用户将通过Ajax高效加载:

form_ajax_refs={'author':{'fields':(User.name,User.email),},}这个指令告诉Flask-Admin,当我们查找作者时,它应该允许我们在作者的姓名或电子邮件上进行搜索。以下屏幕截图显示了它的外观:

我们现在有一个非常漂亮的Entry表单。

因为密码在数据库中以哈希形式存储,直接显示或编辑它们的价值很小。然而,在User表单上,我们将使输入新密码来替换旧密码成为可能。就像我们在Entry表单上对status字段所做的那样,我们将指定一个表单字段覆盖。然后,在模型更改处理程序中,我们将在保存时更新用户的密码。

对UserModelView模块进行以下添加:

fromwtforms.fieldsimportPasswordField#Attopofmodule.classUserModelView(ModelView):column_filters=('email','name','active')column_list=['email','name','active','created_timestamp']column_searchable_list=['email','name']form_columns=['email','password','name','active']form_extra_fields={'password':PasswordField('Newpassword'),}defon_model_change(self,form,model,is_created):ifform.password.data:model.password_hash=User.make_password(form.password.data)returnsuper(UserModelView,self).on_model_change(form,model,is_created)以下截图显示了新的User表单的样子。如果您希望更改用户的密码,只需在新密码字段中输入新密码即可。

仍然有一个方面需要解决。当创建新的Entry、User或Tag对象时,Flask-Admin将无法正确生成它们的slug。这是由于Flask-Admin在保存时实例化新模型实例的方式。为了解决这个问题,我们将创建一些ModelView的子类,以确保为Entry、User和Tag对象正确生成slug。

打开admin.py文件,并在模块顶部添加以下类:

classBaseModelView(ModelView):passclassSlugModelView(BaseModelView):defon_model_change(self,form,model,is_created):model.generate_slug()returnsuper(SlugModelView,self).on_model_change(form,model,is_created)这些更改指示Flask-Admin,每当模型更改时,应重新生成slug。

为了开始使用这个功能,更新EntryModelView和UserModelView模块以扩展SlugModelView类。对于Tag模型,直接使用SlugModelView类进行注册即可。

总结一下,您的代码应该如下所示:

Flask-Admin提供了一个方便的界面,用于管理静态资产(或磁盘上的其他文件),作为管理员仪表板的扩展。让我们向我们的网站添加一个FileAdmin,它将允许我们上传或修改应用程序的static目录中的文件。

打开admin.py文件,并在文件顶部导入以下模块:

fromflask.ext.admin.contrib.fileadminimportFileAdmin然后,在各种ModelView实现下,添加以下突出显示的代码行:

classBlogFileAdmin(FileAdmin):passadmin=Admin(app,'BlogAdmin')admin.add_view(EntryModelView(Entry,db.session))admin.add_view(SlugModelView(Tag,db.session))admin.add_view(UserModelView(User,db.session))admin.add_view(BlogFileAdmin(app.config['STATIC_DIR'],'/static/',name='StaticFiles'))在浏览器中打开管理员,您应该会看到一个名为静态文件的新选项卡。单击此链接将带您进入一个熟悉的文件浏览器,如下截图所示:

如果您在管理文件时遇到问题,请确保为static目录及其子目录设置了正确的权限。

第一步是向我们的User模型添加一个新列。将admin列添加到User模型中,如下所示:

classUser(db.Model):id=db.Column(db.Integer,primary_key=True)email=db.Column(db.String(64),unique=True)password_hash=db.Column(db.String(255))name=db.Column(db.String(64))slug=db.Column(db.String(64),unique=True)active=db.Column(db.Boolean,default=True)admin=db.Column(db.Boolean,default=False)created_timestamp=db.Column(db.DateTime,default=datetime.datetime.now)现在我们将使用Flask-Migrate扩展生成模式迁移:

(blog)$pythonmanage.pydbmigrateINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.autogenerate.compare]Detectedaddedcolumn'user.admin'Generating/home/charles/projects/blog/app/migrations/versions/33011181124e_.py...done(blog)$pythonmanage.pydbupgradeINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.migration]Runningupgrade40ce2670e7e2->33011181124e,emptymessage让我们还向User模型添加一个方法,用于告诉我们给定的用户是否是管理员。将以下方法添加到User模型中:

classUser(db.Model):#...defis_admin(self):returnself.admin这可能看起来很傻,但如果您希望更改应用程序确定用户是否为管理员的语义,这是很好的代码规范。

在继续下一节之前,您可能希望修改UserModelView类,将admin列包括在column_list、column_filters和form_columns中。

由于我们在管理员视图中创建了几个视图,我们需要一种可重复使用的表达我们身份验证逻辑的方法。我们将通过组合实现此重用。您已经在视图装饰器(@login_required)的形式中看到了组合-装饰器只是组合多个函数的一种方式。Flask-Admin有点不同,它使用Python类来表示单个视图。我们将使用一种友好于类的组合方法,称为mixins,而不是函数装饰器。

mixin是提供方法覆盖的类。在Flask-Admin的情况下,我们希望覆盖的方法是is_accessible方法。在这个方法内部,我们将检查当前用户是否已经验证。

为了访问当前用户,我们必须在admin模块的顶部导入特殊的g对象:

fromflaskimportg,url_for在导入语句下面,添加以下类:

classAdminAuthentication(object):defis_accessible(self):returng.user.is_authenticatedandg.user.is_admin()最后,我们将通过Python的多重继承将其与其他几个类混合在一起。对BaseModelView类进行以下更改:

classBaseModelView(AdminAuthentication,ModelView):pass还有BlogFileAdmin类:

classBlogFileAdmin(AdminAuthentication,FileAdmin):pass如果尝试访问/admin/entry/等管理员视图URL而不符合is_accessible条件,Flask-Admin将返回HTTP403Forbidden响应,如下截图所示:

由于我们没有对Tag管理员模型进行更改,因此仍然可以访问。我们将由您来解决如何保护它。

我们的管理员着陆页(/admin/)非常无聊。实际上,除了导航栏之外,它根本没有任何内容。Flask-Admin允许我们指定自定义索引视图,我们将使用它来显示一个简单的问候语。

为了添加自定义索引视图,我们需要导入几个新的帮助程序。将以下突出显示的导入添加到admin模块的顶部:

fromflask.ext.adminimportAdmin,AdminIndexView,exposefromflaskimportredirect请求提供@expose装饰器,就像Flask本身使用@route一样。由于这个视图是索引,我们将要暴露的URL是/。以下代码将创建一个简单的索引视图,用于呈现模板。请注意,在初始化Admin对象时,我们将索引视图指定为参数:

classIndexView(AdminIndexView):@expose('/')defindex(self):returnself.render('admin/index.html')admin=Admin(app,'BlogAdmin',index_view=IndexView())最后还缺少一件事:身份验证。由于用户通常会直接访问/admin/来访问管理员,因此检查索引视图中当前用户是否经过身份验证将非常方便。我们可以通过以下方式来检查:当前用户是否经过身份验证。

classIndexView(AdminIndexView):@expose('/')defindex(self):ifnot(g.user.is_authenticatedandg.user.is_admin()):returnredirect(url_for('login',next=request.path))returnself.render('admin/index.html')Flask-Admin模板Flask-Admin提供了一个简单的主模板,您可以扩展它以创建统一的管理员站点外观。Flask-Admin主模板包括以下区块:

对于这个示例,body块对我们来说最有趣。在应用程序的templates目录中,创建一个名为admin的新子目录,其中包含一个名为index.html的空文件。

{%extends"admin/master.html"%}{%blockbody%}

Hello,{{g.user.name}}

{%endblock%}以下是我们新着陆页的截图:

这只是一个例子,用来说明扩展和定制管理面板是多么简单。尝试使用各种模板块,看看是否可以在导航栏中添加一个注销按钮。

在本章中,我们学习了如何使用Flask-Admin扩展为我们的应用程序创建管理面板。我们学习了如何将我们的SQLAlchemy模型公开为可编辑对象的列表,以及如何定制表格和表单的外观。我们添加了一个文件浏览器,以帮助管理应用程序的静态资产。我们还将管理面板与我们的身份验证系统集成。

在下一章中,我们将学习如何向我们的应用程序添加API,以便可以通过编程方式访问它。

对于我们的目的,以下字段应该足够:

让我们通过在我们的应用程序的models.py模块中创建Comment模型定义来开始编码:

classComment(db.Model):STATUS_PENDING_MODERATION=0STATUS_PUBLIC=1STATUS_SPAM=8STATUS_DELETED=9id=db.Column(db.Integer,primary_key=True)name=db.Column(db.String(64))email=db.Column(db.String(64))url=db.Column(db.String(100))ip_address=db.Column(db.String(64))body=db.Column(db.Text)status=db.Column(db.SmallInteger,default=STATUS_PUBLIC)created_timestamp=db.Column(db.DateTime,default=datetime.datetime.now)entry_id=db.Column(db.Integer,db.ForeignKey('entry.id'))def__repr__(self):return''%(self.name,)在添加Comment模型定义之后,我们需要设置Comment和Entry模型之间的SQLAlchemy关系。您会记得,我们在设置User和Entry之间的关系时曾经做过一次,通过entries关系。我们将通过在Entry模型中添加一个comments属性来为Comment做这个。

在tags关系下面,添加以下代码到Entry模型定义中:

classEntry(db.Model):#...tags=db.relationship('Tag',secondary=entry_tags,backref=db.backref('entries',lazy='dynamic'))comments=db.relationship('Comment',backref='entry',lazy='dynamic')我们已经指定了关系为lazy='dynamic',正如您从第五章验证用户中所记得的那样,这意味着在任何给定的Entry实例上,comments属性将是一个可过滤的查询。

为了开始使用我们的新模型,我们需要更新我们的数据库模式。使用manage.py助手,为Comment模型创建一个模式迁移:

(blog)$pythonmanage.pydbmigrateINFO[alembic.migration]ContextimplSQLiteImpl.INFO[alembic.migration]Willassumenon-transactionalDDL.INFO[alembic.autogenerate.compare]Detectedaddedtable'comment'Generating/home/charles/projects/blog/app/migrations/versions/490b6bc5f73c_.py...done然后通过运行upgrade来应用迁移:

有了我们的模型,我们现在准备安装Flask-Restless,这是一个第三方Flask扩展,可以简单地为您的SQLAlchemy模型构建RESTfulAPI。确保您已经激活了博客应用的虚拟环境后,使用pip安装Flask-Restless:

(blog)$pipinstallFlask-Restless您可以通过打开交互式解释器并获取已安装的版本来验证扩展是否已安装。不要忘记,您的确切版本号可能会有所不同。

(blog)$./manage.pyshellIn[1]:importflask_restlessIn[2]:flask_restless.__version__Out[2]:'0.13.0'现在我们已经安装了Flask-Restless,让我们配置它以使其与我们的应用程序一起工作。

像其他Flask扩展一样,我们将从app.py模块开始,通过配置一个将管理我们新API的对象。在Flask-Restless中,这个对象称为APIManager,它将允许我们为我们的SQLAlchemy模型创建RESTful端点。将以下行添加到app.py:

#Placethisimportatthetopofthemodulealongsidetheotherextensions.fromflask.ext.restlessimportAPIManager#Placethislinebelowtheinitializationoftheappanddbobjects.api=APIManager(app,flask_sqlalchemy_db=db)因为API将依赖于我们的FlaskAPI对象和我们的Comment模型,所以我们需要确保我们不创建任何循环模块依赖关系。我们可以通过在应用程序目录的根目录下创建一个新模块“api.py”来避免引入循环导入。

让我们从最基本的开始,看看Flask-Restless提供了什么。在api.py中添加以下代码:

最后的操作是在main.py中导入新的API模块,这是我们应用程序的入口点。我们导入模块纯粹是为了它的副作用,注册URL路由。在main.py中添加以下代码:

fromappimportapp,dbimportadminimportapiimportmodelsimportviews...发出API请求在一个终端中,启动开发服务器。在另一个终端中,让我们看看当我们向我们的API端点发出GET请求时会发生什么(注意没有尾随的斜杠):

在条目蓝图的表单模块中,添加以下表单定义:

classCommentForm(wtforms.Form):name=wtforms.StringField('Name',validators=[validators.DataRequired()])email=wtforms.StringField('Email',validators=[validators.DataRequired(),validators.Email()])url=wtforms.StringField('URL',validators=[validators.Optional(),validators.URL()])body=wtforms.TextAreaField('Comment',validators=[validators.DataRequired(),validators.Length(min=10,max=3000)])entry_id=wtforms.HiddenField(validators=[validators.DataRequired()])defvalidate(self):ifnotsuper(CommentForm,self).validate():returnFalse#Ensurethatentry_idmapstoapublicEntry.entry=Entry.query.filter((Entry.status==Entry.STATUS_PUBLIC)&(Entry.id==self.entry_id.data)).first()ifnotentry:returnFalsereturnTrue你可能会想为什么我们要指定验证器,因为API将处理POST的数据。我们这样做是因为Flask-Restless不提供验证,但它提供了一个我们可以执行验证的钩子。这样,我们就可以在我们的RESTAPI中利用WTForms验证。

为了在条目详细页面使用表单,我们需要在渲染详细模板时将表单传递到上下文中。打开条目蓝图并导入新的CommentForm:

fromentries.formsimportEntryForm,ImageForm,CommentForm然后修改“详细”视图,将一个表单实例传递到上下文中。我们将使用请求的条目的值预填充entry_id隐藏字段:

@entries.route('//')defdetail(slug):entry=get_entry_or_404(slug)form=CommentForm(data={'entry_id':entry.id})returnrender_template('entries/detail.html',entry=entry,form=form)现在表单已经在详细模板上下文中,剩下的就是渲染表单。在entries/templates/entries/includes/中创建一个空模板,命名为comment_form.html,并添加以下代码:

在statics/js/中创建一个名为comments.js的新文件,并添加以下JavaScript代码:

在detail.html模板中,我们只需要包含我们的脚本并绑定提交处理程序。在详细模板中添加以下块覆盖:

不幸的是,我们的API没有对传入数据进行任何类型的验证。为了验证POST数据,我们需要使用Flask-Restless提供的一个钩子。Flask-Restless将这些钩子称为请求预处理器和后处理器。

我们刚刚看了一个使用Flask-Restless的POST方法预处理器的示例。在下表中,你可以看到其他可用的钩子:

让我们在Comment模型上添加一个方法来生成用户Gravatar图像的URL。打开models.py并向Comment添加以下方法:

如果我们尝试在列的列表中包括Gravatar,Flask-Restless会引发异常,因为gravatar实际上是一个方法。幸运的是,Flask-Restless提供了一种在序列化对象时包含方法调用结果的方法。在api.py中,对create_api()的调用进行以下添加:

打开comments.js并在以下行之后添加以下代码:

最后的任务是在页面呈现时在详细模板中调用Comments.load()。打开detail.html并添加以下突出显示的代码:

Flask-Restless支持许多配置选项,由于篇幅原因,本章未能涵盖。搜索过滤器是一个非常强大的工具,我们只是触及了可能性的表面。此外,预处理和后处理钩子可以用于实现许多有趣的功能,例如以下功能:

在下一章中,我们将致力于创建可测试的应用程序,并找到改进我们代码的方法。这也将使我们能够验证我们编写的代码是否按照我们的意愿进行操作;不多,也不少。自动化这一过程将使您更有信心,并确保RESTfulAPI按预期工作。

在本章中,我们将学习如何编写覆盖博客应用程序所有部分的单元测试。我们将利用Flask的测试客户端来模拟实时请求,并了解Mock库如何简化测试复杂交互,比如调用数据库等第三方服务。

在本章中,我们将学习以下主题:

单元测试是一个让我们对代码、bug修复和未来功能有信心的过程。单元测试的理念很简单;你编写与你的功能代码相辅相成的代码。

举个例子,假设我们设计了一个需要正确计算一些数学的程序;你怎么知道它成功了?为什么不拿出一个计算器,你知道计算机是什么吗?一个大计算器。此外,计算机在乏味的重复任务上确实非常擅长,那么为什么不编写一个单元测试来为你计算出答案呢?对代码的所有部分重复这种模式,将这些测试捆绑在一起,你就对自己编写的代码完全有信心了。

有人说测试是代码“味道”的标志,你的代码如此复杂,以至于需要测试来证明它的工作。这意味着代码应该更简单。然而,这真的取决于你的情况,你需要自己做出判断。在我们开始简化代码之前,单元测试是一个很好的起点。

单元测试的巧妙之处在于测试与功能代码相辅相成。这些方法证明了测试的有效性,而测试证明了方法的有效性。它减少了代码出现重大功能错误的可能性,减少了将来重新编写代码的头痛,并允许你专注于你想要处理的新功能的细枝末节。

单元测试的理念是验证代码的小部分,或者说是测试简单的功能部分。这将构建成应用程序的整体。很容易写出大量测试代码,测试的是代码的功能而不是代码本身。如果你的测试看起来很大,通常表明你的主要代码应该被分解成更小的方法。

幸运的是,几乎总是如此,Python有一个内置的单元测试模块。就像Flask一样,很容易放置一个简单的单元测试模块。在你的主要博客应用程序中,创建一个名为tests的新目录,并在该目录中创建一个名为test.py的新文件。现在,使用你喜欢的文本编辑器,输入以下代码:

importunittestclassExampleTest(unittest.TestCase):defsetUp(self):passdeftearDown(self):passdeftest_some_functionality(self):passdeftest_some_other_functionality(self):passif__name__=="__main__":unittest.main()前面的片段演示了我们将编写的所有单元测试模块的基本框架。它简单地利用内置的Python模块unittest,然后创建一个包装特定测试集的类。在这个例子中,测试是以单词test开头的方法。单元测试模块将这些方法识别为每次调用unittest.main时应该运行的方法。此外,TestCase类(ExampleTest类在这里继承自它)具有一些特殊方法,单元测试将始终尝试使用。其中之一是setUp,这是在运行每个测试方法之前运行的方法。当您想要在隔离环境中运行每个测试,但是,例如,要在数据库中建立连接时,这可能特别有用。

另一个特殊的方法是tearDown。每次运行测试方法时都会运行此方法。同样,当我们想要维护数据库时,这对于每个测试都在隔离环境中运行非常有用。

显然,这个代码示例如果运行将不会做任何事情。要使其处于可用状态,并且遵循测试驱动开发(TDD)的原则,我们首先需要编写一个测试,验证我们即将编写的代码是否正确,然后编写满足该测试的代码。

在这个示例中,我们将编写一个测试,验证一个方法将接受两个数字作为参数,从第二个参数中减去一个,然后将它们相乘。看一下以下示例:

在你的test.py文件中,你可以创建一个在ExampleTest类中表示前面表格的方法,如下所示:

deftest_minus_one_multiplication(self):self.assertEqual(my_multiplication(1,1),0)self.assertEqual(my_multiplication(1,2),1)self.assertEqual(my_multiplication(2,3),4)self.assertNotEqual(my_multiplication(2,2),3)前面的代码创建了一个新的方法,使用Python的unittest模块来断言问题的答案。assertEqual函数将my_multiplication方法返回的响应作为第一个参数,并将其与第二个参数进行比较。如果通过了,它将什么也不做,等待下一个断言进行测试。但如果不匹配,它将抛出一个错误,并且你的测试方法将停止执行,告诉你出现了错误。

在前面的代码示例中,还有一个assertNotEqual方法。它的工作方式与assertEqual类似,但是检查值是否不匹配。还有一个好主意是检查你的方法何时可能失败。如果你只检查了方法将起作用的情况,那么你只完成了一半的工作,并且可能会在边缘情况下遇到问题。Python的unittest模块提供了各种各样的断言方法,这将是有用的去探索。

现在我们可以编写将给出这些结果的方法。为简单起见,我们将在同一个文件中编写该方法。在文件中,创建以下方法:

defmy_multiplication(value1,value2):returnvalue1*value2–1保存文件并使用以下命令运行它:

哎呀!它失败了。为什么?嗯,回顾my_multiplication方法发现我们漏掉了一些括号。让我们回去纠正一下:

defmy_multiplication(value1,value2):returnvalue1*(value2–1)现在让我们再次运行它:

成功了!现在我们有了一个正确的方法;将来,我们将知道它是否被更改过,以及在以后需要如何更改。现在来用这个新技能与Flask一起使用。

你可能会想:“单元测试对于代码的小部分看起来很棒,但是如何为整个Flask应用程序进行测试呢?”嗯,正如之前提到的一种方法是确保所有的方法尽可能离散——也就是说,确保你的方法尽可能少地完成它们的功能,并避免方法之间的重复。如果你的方法不是离散的,现在是整理它们的好时机。

另一件有用的事情是,Flask已经准备好进行单元测试。任何现有应用程序都有可能至少可以应用一些单元测试。特别是,任何API区域,例如无法验证的区域,都可以通过利用Flask中已有的代表HTTP请求的方法来进行极其容易的测试。以下是一个简单的示例:

importunittestfromflaskimportrequestfrommainimportappclassAppTest(unittest.TestCase):defsetUp(self):self.app=app.test_client()deftest_homepage_works(self):response=self.app.get("/")self.assertEqual(response.status_code,200)if__name__=="__main__":unittest.main()这段代码应该看起来非常熟悉。它只是重新编写了前面的示例,以验证主页是否正常工作。Flask公开的test_client方法允许通过代表HTTP调用的方法简单访问应用程序,就像test方法的第一行所示。test方法本身并不检查页面的内容,而只是检查页面是否成功加载。这可能听起来微不足道,但知道主页是否正常工作是很有用的。结果呢?你可以在这里看到:

需要注意的一件事是,我们不需要测试Flask本身,必须避免测试它,以免为自己创造太多工作。

这是单元测试开始变成功能测试的部分。虽然这本身并没有什么错,但值得注意的是,较小的测试更好。

在团队中编写测试或在生产环境中编写测试时遇到的第一个障碍之一是,我们如何确保测试在不干扰生产甚至开发数据库的情况下运行。您肯定不希望尝试修复错误或试验新功能,然后发现它所依赖的数据已经发生了变化。有时,只需要在本地数据库的副本上运行一个快速测试,而不受任何其他人的干扰,Flask应用程序知道如何使用它。

Flask内置的一个功能是根据环境变量加载配置文件。

app.config.from_envvar('FLASK_APP_BLOG_CONFIG_FILE')前面的方法调用通知您的Flask应用程序应该加载在环境变量FLASK_APP_BLOG_CONFIG_FILE中指定的文件中的配置。这必须是要加载的文件的绝对路径。因此,当您运行测试时,应该在这里引用一个特定于运行测试的文件。

由于我们已经为我们的环境设置了一个配置文件,并且正在创建一个测试配置文件,一个有用的技巧是利用现有的配置并覆盖重要的部分。首先要做的是创建一个带有init.py文件的config目录。然后可以将我们的testing.py配置文件添加到该目录中,并覆盖config.py配置文件的一些方面。例如,你的新测试配置文件可能如下所示:

TESTING=TrueDATABASE="sqlite://上面的代码添加了TESTING属性,可以用来确定你的应用程序当前是否正在进行测试,并将DATABASE值更改为更适合测试的数据库,一个内存中的SQLite数据库,不必在测试结束后清除。

然后这些值可以像Flask中的任何其他配置一样使用,并且在运行测试时,可以指定环境变量指向该文件。如果我们想要自动更新测试的环境变量,我们可以在test文件夹中的test.py文件中更新Python的内置OS环境变量对象:

importosos.environ['FLASK_APP_BLOG_CONFIG_FILE']=os.path.join(os.getcwd(),"config","testing.py")模拟对象模拟是测试人员工具箱中非常有用的一部分。模拟允许自定义对象被一个对象覆盖,该对象可以用来验证方法对其参数是否执行正确的操作。有时,这可能需要重新构想和重构你的应用程序,以便以可测试的方式工作,但是概念很简单。我们创建一个模拟对象,将其运行通过方法,然后对该对象运行测试。它特别适用于数据库和ORM模型,比如SQLAlchemy。

有很多模拟框架可用,但是在本书中,我们将使用Mockito:

pipinstallmockito这是最简单的之一:

>>>frommockitoimport*>>>mock_object=mock()>>>mock_object.example()>>>verify(mock_object).example()True上面的代码从Mockito库导入函数,创建一个可以用于模拟的mock对象,对其运行一个方法,并验证该方法已经运行。显然,如果你希望被测试的方法在没有错误的情况下正常运行,你需要在调用模拟对象上的方法时返回一个有效的值。

>>>duck=mock()>>>when(duck).quack().thenReturn("quack")>>>duck.quack()"quack"在上面的例子中,我们创建了一个模拟的duck对象,赋予它quack的能力,然后证明它可以quack。

在Python这样的动态类型语言中,当你拥有的对象可能不是你期望的对象时,使用鸭子类型是一种常见的做法。正如这句话所说“如果它走起来像鸭子,叫起来像鸭子,那它一定是鸭子”。这在创建模拟对象时非常有用,因为很容易使用一个假的模拟对象而不让你的方法注意到切换。

当Flask使用其装饰器在你的方法运行之前运行方法,并且你需要覆盖它,例如,替换数据库初始化程序时,就会出现困难。这里可以使用的技术是让装饰器运行一个对模块全局可用的方法,比如创建一个连接到数据库的方法。

假设你的app.py看起来像下面这样:

fromflaskimportFlask,gapp=Flask("example")defget_db():return{}@app.before_requestdefsetup_db():g.db=get_db()@app.route("/")defhomepage():returng.db.get("foo")上面的代码设置了一个非常简单的应用程序,创建了一个Python字典对象作为一个虚假的数据库。现在要覆盖为我们自己的数据库如下:

frommockitoimport*importunittestimportappclassFlaskExampleTest(unittest.TestCase):defsetUp(self):self.app=app.app.test_client()self.db=mock()defget_fake_db():returnself.dbapp.get_db=get_fake_dbdeftest_before_request_override(self):when(self.db).get("foo").thenReturn("123")response=self.app.get("/")self.assertEqual(response.status_code,200)self.assertEqual(response.data,"123")if__name__=="__main__":unittest.main()上面的代码使用Mockito库创建一个虚假的数据库对象。它还创建了一个方法,覆盖了app模块中创建数据库连接的方法,这里是一个简单的字典对象。你会注意到,当使用Mockito时,你也可以指定方法的参数。现在当测试运行时,它会向数据库插入一个值,以便页面返回;然后进行测试。

记录和错误报告对于一个生产就绪的网络应用来说是内在的。即使你的应用程序崩溃,记录仍然会记录所有问题,而错误报告可以直接通知我们特定的问题,即使网站仍在运行。

在任何人报告错误之前发现错误可能是非常令人满意的。这也使得您能够在用户开始向您抱怨之前推出修复。然而,为了做到这一点,您需要知道这些错误是什么,它们是在什么时候发生的,以及是什么导致了它们。

幸运的是,现在您应该非常熟悉,Python和Flask已经掌握了这一点。

Flask自带一个内置的记录器——Python内置记录器的一个已定义实例。你现在应该对它非常熟悉了。默认情况下,每次访问页面时都会显示记录器消息。

前面的屏幕截图显然显示了终端的输出。我们可以在这里看到有人在特定日期从localhost(127.0.0.1)访问了根页面,使用了GET请求,以及其他一些目录。服务器响应了一个“200成功”消息和两个“404未找到错误”消息。虽然在开发时拥有这个终端输出是有用的,但如果您的应用程序在生产环境中运行时崩溃,这并不一定很有用。我们需要从写入的文件中查看发生了什么。

有各种各样依赖于操作系统的将这样的日志写入文件的方法。然而,如前所述,Python已经内置了这个功能,Flask只是遵循Python的计划,这是非常简单的。将以下内容添加到app.py文件中:

fromlogging.handlersimportRotatingFileHandlerfile_handler=RotatingFileHandler('blog.log')app.logger.addHandler(file_handler)需要注意的一点是,记录器使用不同的处理程序来完成其功能。我们在这里使用的处理程序是RotatingFileHandler。这个处理程序不仅会将文件写入磁盘(在这种情况下是blog.log),还会确保我们的文件不会变得太大并填满磁盘,潜在地导致网站崩溃。

在尝试调试难以追踪的问题时,一个非常有用的事情是我们可以向我们的博客应用程序添加更多的日志记录。这可以通过Flask内置的日志对象来实现,如下所示:

日志级别的原则是:日志的重要性越高,级别越高,根据您的日志级别,记录的可能性就越小。例如,要能够记录警告(以及以上级别,如ERROR),我们需要将日志级别调整为WARNING。我们可以在配置文件中进行这样的调整。编辑config文件夹中的config.py文件,添加以下内容:

importloggingLOG_LEVEL=logging.WARNINGNowinyourapp.pyaddtheline:app.logger.setLevel(config['LOG_LEVEL'])前面的代码片段只是使用内置的Python记录器告诉Flask如何处理日志。当然,您可以根据您的环境设置不同的日志级别。例如,在config文件夹中的testing.py文件中,我们应该使用以下内容:

LOG_LEVEL=logging.ERROR至于测试的目的,我们不需要警告。同样,我们应该为任何生产配置文件做同样的处理;对于任何开发配置文件,使用样式。

在机器上记录错误是很好的,但如果错误直接发送到您的收件箱,您可以立即收到通知,那就更好了。幸运的是,像所有这些东西一样,Python有一种内置的方法可以做到这一点,Flask可以利用它。这只是另一个处理程序,比如RotatingFileHandler。

在下一章中,我们将学习如何通过扩展来改进我们的博客,这些扩展可以在我们的部分付出最小的努力的情况下添加额外的功能。

在本章中,我们将学习如何通过一些流行的第三方扩展增强我们的Flask安装。扩展允许我们以非常少的工作量添加额外的安全性或功能,并可以很好地完善您的博客应用程序。我们将研究跨站点请求伪造(CSRF)保护您的表单,Atom订阅源以便其他人可以找到您的博客更新,为您使用的代码添加语法高亮,减少渲染模板时的负载的缓存,以及异步任务,以便您的应用程序在进行密集操作时不会变得无响应。

在本章中,我们将学习以下内容:

CSRF保护实际上证明了包含CSRF字段的模板用于生成表单。这可以减轻来自其他站点的最基本的CSRF攻击,但不能确定表单提交只来自我们的服务器。例如,脚本仍然可以屏幕抓取页面的内容。

现在,自己构建CSRF保护并不难,而且通常用于生成我们的表单的WTForms已经内置了这个功能。但是,让我们来看看SeaSurf:

pipinstallflask-seasurf安装SeaSurf并使用WTForms后,将其集成到我们的应用程序中现在变得非常容易。打开您的app.py文件并添加以下内容:

fromflask.ext.seasurfimportSeaSurfcsrf=SeaSurf(app)这只是为您的应用程序启用了SeaSurf。现在,要在您的表单中启用CSRF,请打开forms.py并创建以下Mixin:

classLoginForm(Form,CSRFMixin):就是这样。我们需要对所有要保护的表单进行这些更改,通常是所有表单。

任何博客都非常有用的一个功能是让读者能够及时了解最新内容。这通常是通过RSS阅读器客户端来实现的,它会轮询您的RSS订阅源。虽然RSS被广泛使用,但更好、更成熟的订阅格式是可用的,称为Atom。

这两个文件都可以由客户端请求,并且是标准和简单的XML数据结构。幸运的是,Flask内置了Atom订阅源生成器;或者更具体地说,Flask使用的WSGI接口中内置了一个贡献的模块,称为Werkzeug。

让它运行起来很简单,我们只需要从数据库中获取最近发布的帖子。最好为此创建一个新的Blueprint;但是,您也可以在main.py中完成。我们只需要利用一些额外的模块:

fromurlparseimporturljoinfromflaskimportrequest,url_forfromwerkzeug.contrib.atomimportAtomFeedfrommodelsimportEntry并创建一个新的路由:

通常,作为编码人员,我们希望能够在网页上显示代码,虽然不使用语法高亮显示阅读代码是一种技能,但一些颜色可以使阅读体验更加愉快。

与Python一样,已经有一个模块可以为您完成这项工作,当然,您可以通过以下命令轻松安装它:

pipinstallPygments注意Pygments仅适用于已知的代码部分。因此,如果您想显示代码片段,我们可以这样做。但是,如果您想突出显示代码的内联部分,我们要么遵循Markdown的下一节,要么需要使用一些在线Javascript,例如highlight.js。

要创建代码片段,我们需要首先创建一个新的蓝图。让我们创建一个名为snippets的目录,然后创建一个__init__.py文件,接着创建一个名为blueprint.py的文件,其中包含以下代码:

{%extends"base.html"%}{%blocktitle%}{{entry.title}}-Snippets{%endblock%}{%blockcontent_title%}Snippet{%endblock%}{%blockcontent%}{{entry.body|pygments|safe}}{%endblock%}这基本上与我们在书中早期使用的detail.html相同,只是现在我们通过我们在应用程序中创建的Pygments过滤器传递它。由于我们早期使用的模板过滤器生成原始HTML,我们还需要将其输出标记为安全。

我们还需要更新博客的CSS文件,因为Pygments使用CSS选择器来突出显示单词,而不是在页面上浪费地编写输出。它还允许我们根据需要修改颜色。要找出我们的CSS应该是什么样子,打开Pythonshell并运行以下命令:

>>>frompygments.formattersimportHtmlFormatter>>>printHtmlFormatter().get_style_defs('.highlight')前面的命令现在将打印出Pygments建议的示例CSS,我们可以将其复制粘贴到static目录中的.css文件中。

这段代码的其余部分与之前的Entry对象没有太大不同。它只是允许您创建、更新和查看代码片段。您会注意到我们在这里使用了一个SnippetForm,我们稍后会定义。

还要创建一个models.py,其中包含以下内容:

classSnippet(db.Model):STATUS_PUBLIC=0STATUS_DRAFT=1id=db.Column(db.Integer,primary_key=True)title=db.Column(db.String(100))slug=db.Column(db.String(100),unique=True)body=db.Column(db.Text)status=db.Column(db.SmallInteger,default=STATUS_PUBLIC)created_timestamp=db.Column(db.DateTime,default=datetime.datetime.now)modified_timestamp=db.Column(db.DateTime,default=datetime.datetime.now,onupdate=datetime.datetime.now)def__init__(self,*args,**kwargs):super(Snippet,self).__init__(*args,**kwargs)#Callparentconstructor.self.generate_slug()defgenerate_slug(self):self.slug=''ifself.title:self.slug=slugify(self.title)def__repr__(self):return''%self.title现在我们必须重新运行create_db.py脚本以创建新表。

我们还需要创建一个新的表单,以便可以创建代码片段。在forms.py中添加以下代码:

frommodelsimportSnippetclassSnippetForm(wtforms.Form):title=wtforms.StringField('Title',validators=[DataRequired()])body=wtforms.TextAreaField('Body',validators=[DataRequired()])status=wtforms.SelectField('Entrystatus',choices=((Snippet.STATUS_PUBLIC,'Public'),(Snippet.STATUS_DRAFT,'Draft')),coerce=int)defsave_entry(self,entry):self.populate_obj(entry)entry.generate_slug()returnentry最后,我们需要确保通过编辑main.py文件使用此蓝图并添加以下内容:

fromsnippets.blueprintimportsnippetsapp.register_blueprint(snippets,url_prefix='/snippets')一旦我们在这里添加了一些代码,使用Snippet模型,生成的代码将如下图所示呈现:

Markdown的一个有趣之处在于,您仍然可以同时使用HTML和Markdown。

当然,在Python中快速简单地运行这个是很容易的。我们按照以下步骤安装它:

sudopipinstallFlask-Markdown然后我们可以将其应用到我们的蓝图或应用程序中,如下所示:

fromflaskext.markdownimportMarkdownMarkdown(app)这将在我们的模板中创建一个名为markdown的新过滤器,并且在渲染模板时可以使用它:

{{entry.body|markdown}}现在,您只需要在Markdown中编写并保存您的博客条目内容。

如前所述,您可能还希望美化代码块;Markdown内置了这个功能,因此我们需要扩展先前的示例如下:

fromflaskext.markdownimportMarkdownMarkdown(app,extensions=['codehilite'])现在可以使用Pygments来渲染Markdown代码块。但是,由于Pygments使用CSS为代码添加颜色,我们需要从Pygments生成我们的CSS。但是,这次使用的父块具有一个名为codehilite的类(之前称为highlight),因此我们需要进行调整。在Pythonshell中,键入以下内容:

>>>frompygments.formattersimportHtmlFormatter>>>printHtmlFormatter().get_style_defs('.codehilite')现在将输出添加到static目录中的.css文件中。因此,使用包含的CSS,您的Markdown条目现在可能如下所示:

还有许多其他内置的Markdown扩展可以使用;您可以查看它们,只需在初始化Markdown对象时使用它们的名称作为字符串。

有时(我知道很难想象),我们会为我们的网站付出很多努力,添加功能,这通常意味着我们最终不得不为一个简单的静态博客条目执行大量数据库调用或复杂的模板渲染。现在数据库调用不应该很慢,大量模板渲染也不应该引人注目,但是,如果将其扩展到大量用户(希望您是在预期的),这可能会成为一个问题。

因此,如果网站大部分是静态的,为什么不将响应存储在单个高速内存数据存储中呢?无需进行昂贵的数据库调用或复杂的模板渲染;对于相同的输入或路径,获取相同的内容,而且更快。

正如现在已经成为一种口头禅,我们已经可以在Python中做到这一点,而且就像以下这样简单:

sudopipinstallFlask-Cache要使其运行,请将其添加到您的应用程序或蓝图中:

fromflask.ext.cacheimportCacheapp=Flask(__name__)cache=Cache(app,config={'CACHE_TYPE':'redis'})当然,您还需要安装Redis,这在Debian和Ubuntu系统上非常简单:

sudoapt-getinstallredis-server不幸的是,Redis尚未在RedHat和CentOS的打包系统中提供。但是,您可以从他们的网站上下载并编译Redis

默认情况下,Redis是不安全的;只要我们不将其暴露给我们的网络,这应该没问题,而且对于Flask-Cache,我们不需要进行任何其他配置。但是,如果您希望对其进行锁定,请查看Redis的Flask-Cache配置。

现在我们可以在视图中使用缓存(以及任何方法)。这就像在路由上使用装饰器一样简单。因此,打开一个视图并添加以下内容:

对于低动态内容的高流量网站的一种技术是创建一个简单的静态副本。这对博客非常有效,因为内容通常是静态的,并且每天最多更新几次。但是,您仍然需要为实际上没有变化的内容执行大量数据库调用和模板渲染。

当然,有一个Flask扩展程序可以解决这个问题:Frozen-Flask。Frozen-Flask识别Flask应用程序中的URL,并生成应该在那里的内容。

因此,对于生成的页面,它会生成HTML,对于JavaScript和图像等静态内容,它会将它们提取到一个基本目录中,这是您网站的静态副本,并且可以由您的Web服务器作为静态内容提供。

这样做的另一个好处是,网站的活动版本更加安全,因为无法使用Flask应用程序或Web服务器更改它。

pipinstallFrozen-Flask接下来,我们需要创建一个名为freeze.py的文件。这是一个简单的脚本,可以自动设置Frozen-Flask:

fromflask_frozenimportFreezerfrommainimportappfreezer=Freezer(app)if__name__=='__main__':freezer.freeze()以上代码使用了Frozen-Flask的所有默认设置,并在以下方式运行:

pythonfreeze.py将创建(或覆盖)包含博客静态副本的build目录。

importmodels@freezer.register_generatordefarchive():forpostinmodels.Entry.all():yield{'detail':product.id}Frozen-Flask很聪明,并使用Flask提供的url_for方法来创建静态文件。这意味着url_for方法可用的任何内容都可以被Frozen-Flask使用,如果无法通过正常路由找到。

因此,您可能已经猜到,通过创建静态站点,您会失去一些博客基本原理——这是鼓励交流和辩论的一个领域。幸运的是,有一个简单的解决方案。

Frozen-Flask的另一个问题是,对于分布在网络上的多个作者,您如何管理存储帖子的数据库?每个人都需要相同的最新数据库副本;否则,当您生成站点的静态副本时,它将无法创建所有内容。

如果您都在同一个环境中工作,一个解决方案是在网络内的服务器上运行博客的工作副本,并且在发布时,它将使用集中式数据库来创建博客的已发布版本。

然而,如果您都在不同的地方工作,集中式数据库不是理想的解决方案或无法保护,另一个解决方案是使用基于文件系统的数据库引擎,如SQLite。然后,当对数据库进行更新时,可以通过电子邮件、Dropbox、Skype等方式将该文件传播给其他人。然后,他们可以从本地运行Frozen-Flask创建可发布内容的最新副本。

一个这样的例子是电子邮件。用户可能会请求发送电子邮件,例如重置密码请求,您不希望他们在生成和发送电子邮件时等待页面加载。我们可以将其设置为启动和丢弃操作,并让用户知道该请求正在处理中。

Celery能够摆脱Python的单线程环境的方式是,我们必须单独运行一个Celery代理实例;这会创建Celery所谓的执行实际工作的工作进程。然后,您的Flask应用程序和工作进程通过消息代理进行通信。

显然,我们需要安装Celery,我相信您现在可以猜到您需要的命令是以下命令:

pipinstallcelery现在我们需要一个消息代理服务器。有很多选择;查看Celery的网站以获取支持的选择,但是,由于我们已经在Flask-Cache设置中设置了Redis,让我们使用它。

现在我们需要告诉Celery如何使用Redis服务器。打开Flask应用程序配置文件并添加以下行:

CELERY_BROKER_URL='redis://localhost:6379/0'此配置告诉您的Celery实例在哪里找到它需要与Celery代理通信的消息代理。现在我们需要在我们的应用程序中初始化Celery实例。在main.py文件中添加以下内容:

fromceleryimportCelerycelery=Celery(app.name,broker=app.config['CELERY_BROKER_URL'])这将使用来自Flask配置文件的配置创建一个Celery实例,因此我们还可以从Celery代理访问celery对象并共享相同的设置。

现在我们需要为Celery工作进程做一些事情。在这一点上,我们将利用Flask-Mail库:

pipinstallFlask-Mail我们还需要一些配置才能运行。将以下参数添加到您的Flask配置文件中:

MAIL_SERVER="example.com"MAIL_PORT=25MAIL_USERNAME="email_username"MAIL_PASSWORD="email_password"此配置告诉Flask-Mail您的电子邮件服务器在哪里。很可能默认设置对您来说已经足够好,或者您可能需要更多选项。查看Flask-Mail配置以获取更多选项。

现在让我们创建一个名为tasks.py的新文件,并创建一些要运行的任务,如下所示:

fromflask_mailimportMail,Messagefrommainimportapp,celerymail=Mail(app)@celery.taskdefsend_password_verification(email,verification_code):msg=Message("Yourpasswordresetverificationcodeis:{0}".format(verification_code),sender="from@example.com",recipients=[email])mail.send(msg)这是一个非常简单的消息生成;我们只是生成一封电子邮件,内容是新密码是什么,电子邮件来自哪里(我们的邮件服务器),电子邮件发送给谁,以及假设是用户账户的电子邮件地址,然后发送;然后通过已设置的邮件实例发送消息。

现在我们需要让我们的Flask应用程序利用新的异步能力。让我们创建一个视图,监听被POST到它的电子邮件地址。这可以在与帐户或主应用程序有关的任何蓝图中进行。

importtasks@app.route("/reset-password",methods=['POST'])defreset_password():user_email=request.form.get('email')user=db.User.query.filter(email=user_email).first()ifuser:new_password=db.User.make_password("imawally")user.update({"password_hash":new_password})user.commit()tasks.send_password_verification.delay(user.email,new_password)flash("Verificatione-mailsent")else:flash("Usernotfound.")redirect(url_for('homepage'))前面的视图接受来自浏览器的POST消息,其中包含声称忘记密码的用户的电子邮件。我们首先通过他们的电子邮件地址查找用户,以查看用户是否确实存在于我们的数据库中。显然,在不存在的帐户上重置密码是没有意义的。当然,如果他们不存在,用户将收到相应的消息。

请注意,这不是进行密码重置的最佳解决方案。这只是为了说明您可能希望以简洁的方式执行此操作。密码重置是一个令人惊讶地复杂的领域,有很多事情可以做来提高此功能的安全性和隐私性,例如检查CSRF值,限制调用方法的次数,并使用随机生成的URL供用户重置密码,而不是通过电子邮件发送的硬编码解决方案。

最后,当我们运行Flask应用程序时,我们需要运行Celery代理;否则,几乎不会发生任何事情。不要忘记,这个代理是启动所有异步工作者的进程。我们可以做的最简单的事情就是从Flask应用程序目录中运行以下命令:

celeryd-Amainworker这很简单地启动了Celery代理,并告诉它查找main应用程序中的celery配置,以便它可以找到配置和应该运行的任务。

现在我们可以启动我们的Flask应用程序并发送一些电子邮件。

使用Flask非常有用的一件事是创建一个命令行界面,这样当其他人使用您的软件时,他们可以轻松地使用您提供的方法,比如设置数据库、创建管理用户或更新CSRF密钥。

我们已经有一个类似的脚本,并且可以在这种方式中使用的脚本是第二章中的create_db.py脚本,使用SQLAlchemy的关系数据库。为此,再次有一个Flask扩展。只需运行以下命令:

pipinstallFlask-Script现在,Flask-Script的有趣之处在于,命令的工作方式与Flask中的路由和视图非常相似。让我们看一个例子:

fromflask.ext.scriptimportManagerfrommainimportappmanager=Manager(app)@manager.commanddefhello():print"HelloWorld"if__name__=="__main__":manager.run()您可以在这里看到,Flask-Script将自己称为Manager,但管理器也将自己挂钩到Flask应用程序中。这意味着您可以通过使用app引用来对Flask应用程序执行任何操作。

因此,如果我们将create_db.py应用程序转换为Flask-Script应用程序,我们应该创建一个文件来完成这项工作。让我们称之为manage.py,并从文件create_db.py中插入:

frommainimportdb@manager.commanddefcreate_db():db.create_all()所有这些只是设置一个装饰器,以便manage.py带有参数create_db将运行create_db.py中的方法。

现在我们可以从以下命令行运行:

pythonmanage.pycreate_db参考总结在本章中,我们做了各种各样的事情。您已经看到如何创建自己的Markdown渲染器,以便编辑更容易,并将命令移动到Flask中,使其更易管理。我们创建了Atomfeeds,这样我们的读者可以在发布新内容时找到它,并创建了异步任务,这样我们就不会在等待页面加载时锁定用户的浏览器。

在我们的最后一章中,我们将学习如何将我们的简单应用程序转变为一个完全部署的博客,具有所有讨论的功能,已经得到保护,并且可以使用。

在本章中,我们将学习如何以安全和自动化的可重复方式部署我们的Flask应用程序。我们将看到如何配置常用的WSGI(Web服务器网关接口)能力服务器,如Apache、Nginx,以及PythonWeb服务器Gunicorn。然后,我们将看到如何使用SSL保护部分或整个站点,最后将我们的应用程序包装在配置管理工具中,以自动化我们的部署。

重要的是要注意,Flask本身并不是一个Web服务器。Web服务器是面向互联网的工具,经过多年的开发和修补,并且可以同时运行多个服务。

在互联网上仅运行Flask作为Web服务器可能会很好,这要归功于WerkzeugWSGI层。然而,Flask在页面路由和渲染系统上的真正重点是开发。作为Web服务器运行Flask可能会产生意想不到的影响。理想情况下,Flask将位于Web服务器后面,并在服务器识别到对您的应用程序的请求时被调用。为此,Web服务器和Flask需要能够使用相同的语言进行通信。

然而,要让Werkzeug使用WSGI协议与您的Web服务器通信,我们必须使用一个网关。这将接收来自您的Web服务器和Python应用程序的请求,并在它们之间进行转换。大多数Web服务器都会使用WSGI,尽管有些需要一个模块,有些需要一个单独的网关,如uWSGI。

首先要做的一件事是为WSGI网关创建一个WSGI文件以进行通信。这只是一个具有已知结构的Python文件,以便WSGI网关可以访问它。我们需要在与您的博客应用程序的其余部分相同的目录中创建一个名为wsgi.py的文件,它将包含:

要确保在基于Debian和Ubuntu的系统上安装了Apache和WSGI模块,请运行以下命令:

sudoapt-getinstallapache2libapache2-mod-wsgi但是,在基于RedHat和Fedora的系统上运行以下命令:

在该配置文件中,使用以下代码更新内容:

WSGIScriptAlias//wsgi.py/>Orderdeny,allowAllowfromall此配置指示Apache,对于对端口80上主机的每个请求,都要尝试从wsgi.py脚本加载。目录部分告诉Apache如何处理对该目录的请求,并且默认情况下,最好拒绝任何访问Web服务器的人对源目录中的文件的访问。请注意,在这种情况下,是存储wsgi.py文件的目录的完整绝对路径。

LoadModulewsgi_modulemodules/mod_wsgi.so现在我们需要通过运行以下命令在基于Debian和Ubuntu的系统上启用我们的新站点:

sudoa2ensiteblog这指示Apache在/etc/apache2/sites-available和/etc/apache2/sites-enabled之间创建符号链接,Apache实际上从中获取其配置。现在我们需要重新启动Apache。在您的特定环境或分发中,可以以许多方式执行此操作。最简单的方法可能只是运行以下命令:

请注意,一些Linux发行版默认配置必须禁用。这可能可以通过在Debian和Ubuntu系统中输入以下命令来禁用:

sudoserviceapache2restart在基于RedHat和CentOS的系统中:

在使用Flask时,通过Web服务器,非常重要的一步是通过为站点的静态内容创建一个快捷方式来减少应用程序的负载。这将把相对琐碎的任务交给Web服务器,使得处理过程更快速、更响应。这也是一件简单的事情。

编辑您的blog.conf文件,在标签内添加以下行:

Alias/static/static在这里,是静态目录存在的完整绝对路径。然后按照以下步骤重新加载Debian和Ubuntu系统的Apache配置:

sudoserviceapache2restart对于基于RedHat和CentOS的系统如下:

sudoapt-getinstallnginx在基于RedHat或Fedora的系统中,以下

sudoyuminstallnginx现在由于uWSGI是一个Python模块,我们可以使用pip安装它:

sudopipinstalluwsgi要在基于Debian和Ubuntu的系统中配置Nginx,需要在/etc/nginx/sites-available中创建一个名为blog.conf的文件,或者在基于RedHat或Fedora的系统中,在/etc/nginx/conf.d中创建文件,并添加以下内容:

server{listen80;server_name_;location/{try_files$uri@blogapp;}location@blogapp{includeuwsgi_params;uwsgi_passunix:/var/run/blog.wsgi.sock;}}这个配置与Apache配置非常相似,尽管是以Nginx形式表达的。它在端口80上接受连接,并且对于任何服务器名称,它都会尝试访问blog.wsgi.sock,这是一个用于与uWSGI通信的Unix套接字文件。您会注意到@blogapp被用作指向位置的快捷方式引用。

只有在基于Debian和Ubuntu的系统中,我们现在需要通过从可用站点创建符号链接到已启用站点来启用新站点:

sudoln-s/etc/nginx/sites-available/blog.conf/etc/nginx/sites-enabled然后我们需要告诉uWSGI在哪里找到套接字文件,以便它可以与Nginx通信。为此,我们需要在blogapp目录中创建一个名为uwsgi.ini的uWSGI配置文件,其中包含以下内容:

[uwsgi]base=app=appmodule=appsocket=/var/run/blog.wsgi.sock您将需要将更改为您的app.py文件存在的路径。还要注意套接字是如何设置在与Nginx站点配置文件中指定的相同路径中的。

您可能会注意到INI文件的格式和结构非常类似于Windows的INI文件。

我们可以通过运行以下命令来验证此配置是否有效:

uwsgi–iniuwsgi.ini现在Nginx知道如何与网关通信,但还没有使用站点配置文件;我们需要重新启动它。在您特定的环境中可以通过多种方式执行此操作。最简单的方法可能就是运行以下命令:

请注意,一些Linux发行版附带了必须禁用的默认配置。在基于Debian和Ubuntu的系统以及基于RedHat和CentOS的系统中,通常可以通过删除/etc/nginx/conf.d/default.conf文件来完成此操作。

sudorm/etc/nginx/conf.d/default.conf并重新启动nginx服务:

sudoservicenginxrestart注意Nginx还有一个重新加载选项,而不是重新启动。这告诉服务器再次查看配置文件并与其一起工作。这通常比重新启动更快,并且可以保持现有的连接打开。而重新启动会退出服务器并重新启动,带走打开的连接。重新启动的好处在于它更加明确,并且对于设置目的更加一致。

在使用Flask通过Web服务器时,非常重要的一步是通过为站点上的静态内容创建一个快捷方式,以减轻应用程序的负载。这将使Web服务器从相对琐碎的任务中解脱出来,使得向最终浏览器提供基本文件的过程更快速、更响应。这也是一个简单的任务。

编辑您的blog.conf文件,在server{标签内添加以下行:

location/static{root/static;}其中是静态目录存在的完整绝对路径。重新加载Nginx配置:

sudoservicenginxrestart这将告诉Nginx在浏览器请求/static时在哪里查找文件。您可以通过查看Nginx日志文件/var/log/nginx/access.log来看到这一点。

Gunicorn是一个用Python编写的Web服务器。它已经理解了WSGI,Flask也是如此,因此让Gunicorn运行起来就像输入以下代码一样简单:

pipinstallgunicorngunicornapp:app其中app:app是您的应用程序,模块名称是我们在其中使用的(与uWSGI配置基本相同)。除此之外还有更多选项,但例如,从中工作并设置端口和绑定是有用的:

gunicorn--bind127.0.0.1:8000app:app--bind标志告诉Gunicorn要连接到哪个接口以及在哪个端口。如果我们只需要在内部使用Web应用程序,这是有用的。

另一个有用的标志是--daemon标志,它告诉Gunicorn在后台运行并与您的shell分离。这意味着我们不再直接控制该进程,但它正在运行,并且可以通过设置的绑定接口和端口进行访问。

在一个日益残酷的互联网上,通过证明其真实性来提高网站的安全性是很重要的。改善网站安全性的常用工具是使用SSL,甚至更好的是TLS。

SSL和TLS证书允许您的服务器通过受信任的第三方基于您的浏览器连接的域名进行验证。这意味着,作为网站用户,我们可以确保我们正在交谈的网站在传输过程中没有被更改,是我们正在交谈的正确服务器,并且在服务器和我们的浏览器之间发送的数据不能被嗅探。当我们想要验证用户发送给我们的信息是否有效和受保护时,这显然变得重要,而我们的用户希望知道我们的数据在传输过程中受到保护。

首先要做的是生成您的SSL证书请求。这与第三方一起使用,该第三方签署请求以验证您的服务器与任何浏览器。有几种方法可以做到这一点,取决于您的系统,但最简单的方法是运行以下命令:

CountryName(2lettercode)[AU]:GBStateorProvinceName(fullname)[Some-State]:LondonLocalityName(eg,city)[]:LondonOrganizationName(eg,company)[InternetWidgitsPtyLtd]:ExampleCompanyOrganizationalUnitName(eg,section)[]:ITCommonName(eg,YOURname)[]:blog.example.comEmailAddress[]:Achallengepassword[]:Anoptionalcompanyname[]:在这里,您可以看到我们使用blog.example.com作为我们示例域名,我们的博客应用将在该域名下访问。您必须在这里使用您自己的域名。电子邮件地址和密码并不是非常重要的,可以留空,但您应该填写“组织名称”字段,因为这将是您的SSL证书被识别为的名称。如果您不是一家公司,只需使用您自己的名字。

该命令为我们生成了两个文件;一个是private.key文件,这是我们的服务器用来与浏览器签署通信的文件,另一个是public.csr,这是发送给处理服务器和浏览器之间验证的第三方服务的证书请求文件。

公钥/私钥加密是一个广泛但深入研究的主题。鉴于Heartbleed攻击,如果您希望保护服务器,了解这个是值得的。

下一步是使用第三方签署您的public.csr请求。有许多服务可以为您执行此操作,有些免费,有些略有成本;例如Let'sEncrypt等一些服务可以完全免费地自动化整个过程。它们都提供基本相同的服务,但它们可能不会全部内置到所有浏览器中,并且为不同成本的不同程度的支持提供不同程度的支持。

这些服务将与您进行验证过程,要求您的public.csr证书请求,并为您的主机名返回一个已签名的.crt证书文件。

请注意,将您的.crt和.key文件命名为其中申请证书的站点主机名可能会对您有所帮助。在我们的情况下,这将是blog.example.com.crt。

您的新.crt文件和现有的.key文件可以放在服务器的任何位置。但是,通常.crt文件放在/etc/ssl/certs中,而.key文件放在/etc/ssl/private中。

所有正确的文件都放在正确的位置后,我们需要重新打开用于我们的博客服务的现有Apache配置。最好运行一个正常的HTTP和HTTPS服务。但是,由于我们已经努力设置了HTTPS服务,强制执行它以重定向我们的用户是有意义的。这可以通过一个称为HSTS的新规范来实现,但并非所有的Web服务器构建都支持这一点,所以我们将使用重定向。

您可以通过向操作系统的主机文件添加一个条目来在本地机器上运行带有SSL证书的测试域。只是不要忘记在完成后将其删除。

首先要更改的是VirtualHost行上的端口,从默认的HTTP端口80更改为默认的HTTPS端口443:

我们还应该指定服务器的主机名正在使用的SSL证书;因此,在VirtualHost部分添加一个ServerName参数。这将确保证书不会在错误的域中使用。

ServerNameblog.example.com您必须用您将要使用的主机名替换blog.example.com。

我们还需要设置SSL配置,以告诉Apache如何响应:

SSLEngineonSSLProtocol-all+TLSv1+SSLv2SSLCertificateFile/etc/ssl/certs/blog.example.com.crtSSLCertificateKeyFile/etc/ssl/private/blog.example.com.keySSLVerifyClientNone这里的情况是,Apache中的SSL模块被启用,为该站点指定了公共证书和私钥文件,并且不需要客户端证书。禁用默认的SSL协议并启用TLS非常重要,因为TLS被认为比SSL更安全。但是,仍然启用SSLv2以支持旧版浏览器。

现在我们需要测试它。让我们重新启动Apache:

现在它正在工作,最后一步是将普通的HTTP重定向到HTTPS。在配置文件中,再次添加以下内容:

再次重启Apache:

sudoserviceapache2restart现在在网站上用浏览器测试这个配置;验证您被重定向到HTTPS,无论您访问哪个页面。

Nginx的配置非常简单。与Apache配置非常相似,我们需要更改Nginx将监听我们站点的端口。由于HTTPS在端口443上运行,这里的区别在于告诉Nginx期望SSL连接。在配置中,我们必须更新以下行:

listen443ssl;现在要将SSL配置添加到配置的服务器元素中,输入以下内容:

server_nameblog.example.com;ssl_certificate/etc/ssl/certs/blog.example.com.crt;ssl_certificate_key/etc/ssl/private/blog.example.com.key;ssl_protocolsTLSv1SSLv2;这告诉Nginx将此配置应用于对blog.example.com主机名的请求(不要忘记用您自己的替换它),因为我们不希望为不适用的域发送SSL证书。我们还指定了公共证书文件位置和文件系统上的私有SSL密钥文件位置。最后,我们指定了要使用的SSL协议,这意味着启用TLS(被认为比SSL更安全)。但是SSLv2仍然启用以支持旧版浏览器。

现在来测试它。让我们重新启动Nginx服务:

一旦我们证明它正在工作,最后一步是将普通的HTTP重定向到HTTPS。再次在配置文件中添加以下内容:

最后一次,重新启动Nginx:

sudoservicenginxrestart最后,在您被重定向到HTTPS的网站上测试您的浏览器,无论您访问哪个页面。

从0.17版本开始,Gunicorn也添加了SSL支持。要从命令行启用SSL,我们需要一些标志:

gunicorn--bind0.0.0.0:443--certfile/etc/ssl/certs/blog.example.com.crt--keyfile/etc/ssl/private/blog.example.com.key--ssl-version2--ciphersTLSv1app:app这与Nginx和ApacheSSL配置的工作方式非常相似。它指定要绑定的端口,以及在这种情况下的所有接口。然后,它将Gunicorn指向公共证书和私钥文件,并选择在旧版浏览器中使用SSLv2和(通常被认为更安全的)TLS密码协议。

通过在浏览器中输入主机名和HTTPS来测试这个。

现在准备好了,让我们将端口80重定向到端口443。这在Gunicorn中相当复杂,因为它没有内置的重定向功能。一个解决方案是创建一个非常简单的Flask应用程序,在Gunicorn上的端口80启动,并重定向到端口443。这将是一个新的应用程序,带有一个新的app.py文件,其内容如下:

请注意,空字符串对于urlunparse函数很重要,因为它期望一个完整的URL元组,就像由urlparse生成的那样。

您现在可能已经知道如何在Gunicorn中运行这个,尽管如此,要使用的命令如下:

gunicorn--bind0.0.0.0:80app:app现在使用浏览器连接到旧的HTTP主机,您应该被重定向到HTTPS版本。

Ansible是一个配置管理工具。它允许我们以可重复和可管理的方式自动化部署我们的应用程序,而无需每次考虑如何部署我们的应用程序。

Ansible可以在本地和通过SSH工作。您可以使用Ansible的一个聪明之处是让Ansible配置自身。根据您自己的配置,然后可以告诉它部署它需要的其他机器。

然而,我们只需要专注于使用Apache、WSGI和Flask构建我们自己的本地Flask实例。

首先要做的是在我们要部署Flask应用的机器上安装Ansible。由于Ansible是用Python编写的,我们可以通过使用pip来实现这一点:

sudopipinstallansible现在我们有了一个配置管理器,既然配置管理器是用来设置服务器的,让我们建立一个playbook,Ansible可以用来构建整个机器。

在一个新项目或目录中,创建一个名为blog.yml的文件。我们正在创建一个Ansible称为Playbook的文件;它是一个按顺序运行的命令列表,并构建我们在Apache下运行的博客。为简单起见,在这个文件中假定您使用的是一个Ubuntu衍生操作系统:

----hosts:webserversuser:ubuntusudo:Truevars:app_src:../blogapp_dest:/srv/blogtasks:-name:installnecessarypackagesaction:aptpkg=$itemstate=installedwith_items:-apache2-libapache2-mod-wsgi-python-setuptools-name:EnablewsgimoduleforApacheaction:commanda2enmodwsgi-name:BlogappconfigurationforApacheaction:templatesrc=templates/blogdest=/etc/apache/sites-available/blog-name:Copyblogappinaction:copysrc=${app_src}dest=${app_dest}-name:Enablesiteaction:commanda2ensiteblog-name:ReloadApacheaction:servicename=apache2state=reloadedAnsiblePlaybook是一个YAML文件,包含几个部分;主要部分描述了“play”。hosts值描述了后续设置应该应用于哪组机器。user描述了play应该以什么用户身份运行;对于您来说,这应该是Ansible可以运行以安装您的应用程序的用户。sudo设置告诉Ansible以sudo权限运行此play,而不是以root身份运行。

vars部分描述了playbook中常见的变量。这些设置很容易找到,因为它们位于顶部,但也可以在playbook配置中以${example_variable}的格式稍后使用,如果example_variable在这里的vars部分中定义。这里最重要的变量是app_src变量,它告诉Ansible在将应用程序复制到正确位置时在哪里找到我们的应用程序。在这个例子中,我们假设它在一个名为blog的目录中,但对于您来说,它可能位于文件系统的其他位置,您可能需要更新此变量。

最后一个最重要的部分是tasks部分。这告诉Ansible在更新它控制的机器时要运行什么。如果您熟悉Ubuntu,这些任务应该有些熟悉。例如,action:apt告诉apt确保with_items列表中指定的所有软件包都已安装。您将注意到$item变量与pkg参数。$item变量由Ansible自动填充,因为它在with_items命令和apt命令上进行迭代,apt命令使用pkg参数来验证软件包是否已安装。

NameVirtualHost*:80WSGIScriptAlias/{{app_dest}}/wsgi.pyOrderdeny,allowAllowfromall这应该很熟悉,这是Apache部分示例的直接剽窃;但是,我们已经利用了Ansible变量来填充博客应用程序的位置。这意味着,如果我们想将应用程序安装到另一个位置,只需更新app_dest变量即可。

最后,在Playbook任务中,它将我们非常重要的博客应用程序复制到机器上,使用Debian简写在Apache中启用站点,并重新加载Apache,以便可以使用该站点。

所以剩下的就是在那台机器上运行Ansible,并让它为您构建系统。

ansible-playbookblog.yml--connection=local这告诉Ansible运行我们之前创建的Playbook文件blog.yml,并在local连接类型上使用它,这意味着应用于本地机器。

Ansible提示

值得注意的是,这可能不是在大型分布式环境中使用Ansible的最佳方式。首先,您可能希望将其应用于远程机器,或者将Apache配置、ApacheWSGI配置、Flask应用程序配置和博客配置分开成Ansible称为角色的单独文件;这将使它们可重用。

另一个有用的提示是指定使用的配置文件并在Apache中设置静态目录。阅读Ansible文档,了解更多有关改进部署的方法的想法:

在本章中,我们已经看到了许多运行Flask应用程序的方法,包括在多个Web服务器中保护隐私和安全,并提供静态文件以减少Flask应用程序的负载。我们还为Ansible制作了一个配置文件,以实现可重复的应用程序部署,因此,如果需要重新构建机器,这将是一个简单的任务。

THE END
1.图怪兽作图神器在线海报编辑器PS图片制作图怪兽作图神器,是一个在线ps图片编辑器,它相当于ps精简版软件,可提供微信编辑器功能,在线ps照片处理,拼图,图片制作,在线设计,平面设计,海报设计,在线图片处理等功能。图怪兽作图不求人处理简单易用,这款在线图片编辑软件让设计海报模板图片更轻松,帮助企业视觉营销投入成本http://www.818ps.com/
2.告别代码小白,网站模板制作大揭秘!你是否曾在网站模板制作的路上跌跌撞撞,感觉像是在没有地图的迷宫里摸索?如果你对设计一窍不通,但又想拥有一个既美观又实用的网站,恭喜你哈,这篇文章就是为你准备的。 今天,我要和大家分享一个神奇的网站制作工具,它不仅能让网站模板制作变得简单快捷,还能让你的创意得到完美展现。 在开始之前,我们先来聊聊那https://zhuanlan.zhihu.com/p/13328184347
3.可以润色的网站有哪些魔幻之旅:让你的文字焕然一新综合来说,这些网站不仅在语言上给予支持,更在思维上启发创作者,激发其对写作的热情和创新力。打开这些润色平台,写作者将踏上一段魔幻的文字之旅,体验到文字的魅力与力量。通过合理使用这些工具,大家都可以轻松掌握写作技巧,让自己的文字焕然一新,吸引更多读者的关注与喜爱。http://m.gmsw.net/gmswmj/101914221411.html
4.魔力设在线设计使用评测分享简介:魔力设是一个提供在线设计服务的网站,包括海量的设计模板、版权图片素材库等资源。它支持在线选图、在线改图等功能,拥有超过300万+的正版插画素材和摄影圈资源。 产品功能: ? 设计模板:涵盖各类设计需求,如海报、展板、手机海报、营销长图、主图、邀请函、详情页、首图、画册、宣传单等。 ? 设计素材:提https://hao.logosc.cn/p/1579
5.魔力设在线设计魔力设-在线设计官网 魔力设是集版权图库和ps在线设计于一体的找图作图工具,平台拥有20万+涵盖各行业可在线编辑的海报、营销长图、主图、首页、封面、展板、易拉宝等设计模板,50万正版版权图片、插画、png免扣元素、摄影图、艺术字等。不管您属于哪个行业,您都可以在平台快速找到您需要的图片或模板,不管您是否会使用https://openi.cn/sites/229045.html
6.魔力设智能设计工具平台海报设计AI智能PPT魔力设300万+在线海报、PPT、展板、名片、简历、邀请函、小报模板,一键替换文字、图片出图,不用会设计、办公软件,也能轻松做设计。http://www.51mo.com/
7.魔力设高效在线设计网站在线版权图片素材库网站描述词(178个字符): 魔力设是集版权图库和ps在线设计于一体的找图作图工具,平台拥有20万+涵盖各行业可在线编辑的海报、营销长图、主图、首页、封面、展板、易拉宝等设计模板,50万正版版权图片、插画、png免扣元素、摄影图、艺术字等。不管您属于哪个行业,您都可以在平台快速找到您需要的图片或模板,不管您是否https://www.kmphb.net/links/41426.html
8.魔力设智能设计工具平台海报设计AI智能PPT魔力设300万+在线海报、PPT、展板、名片、简历、邀请函、小报模板,一键替换文字、图片出图,不用会设计、办公软件,也能轻松做设计。https://www.molishe.com/
9.魔力设高效在线设计网站魔力设是集版权图库和ps在线设计于一体的找图作图工具,平台拥有20万+涵盖各行业可在线编辑的海报、营销长图、主图、首页、封面、展板、易拉宝等设计模板,50万正版版权图片、插画、png免扣元素、摄影图、艺术字等。不管您属于哪个行业,您都可以在平台快速找到您需要的图片https://pidoutv.com/sites/7962.html
10.魔力设:在线智能海报展板邀请函设计及模板素材下载网站魔力设是什么 魔力设是一个在线智能设计工具网站,为用户提供了丰富多样的设计模板和资源,帮助用户轻松创建各种精美的设计作品,如海报、邀请函、宣传展板、手机海报等。无论你是专业设计师还是初学者,都能在魔力设上找到满足自己需求的设计工具和灵感。 魔力设提供300万+在线设计模板、PSD模板、PNG元素、背景素材等资源https://www.zhanid.com/daohang/molishe.html
11.平台拥有20万+涵盖各行业可在线编辑的海报营销长图主图这段时间,我们会在公众号继续更新网站推荐,请关注公众号 相似资源 收藏 九七电影院-97电影网在线看电影,支持微信微博观看,无需播放器的电影网站,支持迅雷电影下载 九七电影院-97电影网提供最全的最新电视剧,2021最新电影,韩国电视剧、香港TVB电视剧、韩剧、日剧、美剧、综艺的在线观看和剧集交流场所,在线观看分为普https://www.bidianer.com/site/321367
12.玩转Scratch少儿趣味编程Scratch 3.0有两种编辑器:一种是网页版编辑器,可直接在线进行作品编辑与存储;另一种则是离线编辑器,使用者在未联网的情况下也可以在计算机上编辑作品。 · 1.1.1 网页版编辑器 网页版编辑器可以直接在该网站上制作与编辑项目。在浏览器的网址栏中输入Scratch官网,如图1-1所示。图1-2所示为创建项目。 https://www.epubit.com/bookDetails?id=UB834402f74a9e9
13.www.pajsl.com/mokohtml/2024/12/16/一,哦我要哦操我,大奶子猎人 二,十个眼镜av无休,中国小鲜肉组合 三,哪里可以看免费的黄色影片,冯珊珊取外卖被锁 四,花吧hao revival操逼,男吃胸 五,火影淫荡,侧所av网扯 六,骚鸡丝口污肏视频免费网站,触屄舒服 七,闵妮图片 【联系我们】 客服热线:133-2881-646http://www.pajsl.com/mokohtml/2024/12/16/
14.Canva可画在线设计协作平台平面设计作图软件使用魔力工作室系列AI功能,重新定义你的创作方式。试试用AI写作?生成各类职场、自媒体文案,用AI修图功能?美化编辑你的照片或用AI生图随心创作图片。 开始体验 多人协作在线编辑 一键分享链接,或在线评论@团队成员即时交流反馈。邀请同学、同事协作完成小组作业、团队汇报演示文稿?、文档?、白板?脑暴,还可以https://www.canva.cn/
15.4399网页游戏魔力学堂 142服12月20日 14:00 凡人修真 444服12月20日 11:00 凡人修真2 2400服12月20日 10:00 热血三国3 2592服12月20日 10:00 刺沙150服12月20日 10:00 【仙侠】打怪修仙 挂机升级 【传奇】山海异兽 洪荒传奇 【怀旧】自由对战 公平竞技 https://web.4399.com/
16.Axure原型设计工具:交互设计与原型制作实践动态面板的真正魔力在于其与交互动效的结合。设计师可以通过设置面板的“交互”功能,为面板的显示或隐藏添加动画效果。例如,可以设置面板从左侧滑入或从右侧滑出,或者采用淡入淡出的方式显示。 实现这些效果通常需要使用“设置面板状态”动作。设计师可以通过设置不同的交互条件,如鼠标点击、定时器或API请求的响应,来触发https://blog.csdn.net/weixin_35754676/article/details/142718540
17.物品变化教程最低级的产能花,但也是必须的一朵花,单独一朵需要95秒才可以产出一次160魔力的射线,本身的缓存为20(只能在白天产能) 2.夜颠奎 同样,也是低级的产能花,本身缓存也只有20,基本是为了对付晚上没有魔力使用而诞生的,产能是太阳花的二分之一(只能在夜晚使用) https://www.633sy.com/sy/a31544787.html
18.魔力视频编辑教程视频教程在线播放 分集下载 主颜色校正 软件中的基本按钮功能(一) 魔力视频素材剪辑 如何使用马赛克(清晰)_576x432_2.00M_h.264 软件中的基本按钮功能(一) 软件如何调整和裁剪图片 软件如何设置DVD光盘预览的方法 吸附功能_ 魔力视频编辑教程的相关介绍 教程列表: https://www.waitang.com/view/68799.html
19.中关村在线中关村在线是大中华区商业价值受到认可和信赖的IT专业门户网站,提供手机,电脑等科技数码的资讯和行情报价.https://www.zol.com.cn/
20.MAGIXMovieEditPro12e对文件大小没有限制,甚至可编辑4Gb的文件。无损编辑、实时编辑。提供的编辑屏幕界面可适合所有工作,仅选择的设置、或在视频编辑过程中在同模式间切换就可很容易地制作自己的电影。 MAGIX Movie Edit Pro,优秀的视频编辑软件,界面豪华明快,赏心悦目,功能上不输其他视频编辑软件。 https://www.jb51.net/article/8994.htm
21.PDF编辑器在线免费版高效智能远程协同办公下载查阅、编辑、转换等PDF全生命周期功能 通过OCR识别来提取各类电子表单数据 PDF文档版本对比 企业组织、权限管理等一站式综合办公平台 制造业 通过OCR功能修改图纸中的参数 设计图纸转换为PDF格式进行交付和存档 PDF版图纸自由编辑、压缩 PDF页面提取、合并与重组 https://www.foxitsoftware.cn/plus/
22.图片编辑在线图片编辑在线制作图片在线作图凡科快图凡科快图,免费在线图片编辑软件,免下载,丰富版权资源,海量图片设计模板,不用ps,1分钟在线制作图片,超简单3步操作,完成在线作图,支持抠图、压缩、分割、加水印-梦想导航https://nav.dreamthere.cn/site/index/67329
23.inedit:网站内容编辑工具国外精选 网站优化数据分析魔力工作室 AI助力的创意设计平台,提升设计效率。 Canva可画魔力工作室是一个集成了AI工具的在线设计平台,提供从文案生成、图片编辑到动画制作等一系列设计功能。它通过简化设计流程,使个人创作者和团队能够快速实现创意构想,提升工作效率。魔力工作室的AI功能包括文案自动生成、图像智能编辑等https://top.aibase.com/tool/inedit
24.IT培训班青鸟国际影视* 3DMAX建模解界面及操作,几何体、图形创建,常用编辑命令,材质及灯光渲 视觉传达设计动态精英班-哈尔滨唐人空间艺术设 1、唐人空间设计教育Python高级开发培训一、课程简介 通过本课程可以体会与其它语言相比python不一样的地方,重点掌握实际开发中经常使用的模块和技巧,体验python敏捷开发的魔力,它的http://www.024jianzhan.cn/
25.娱乐产业分析(通用6篇)将自己的名字魔力般地塑造成一个面对女性的时尚品牌,涵盖面包括电视节目、网站、图书、商店、礼品、无论是对电影、电视,还是图书、音乐而言,windows都是对各个产业领域相关商品市场的层层挤占和关关设卡,娱乐资讯类节目《新娱乐在线》每晚21:00-22:00给观众带来最新娱乐新闻动态。周末期间上海电视台娱乐https://www.360wenmi.com/f/fileu5mi9r5n.html
26.魔力设一天能下多少张50张。魔力设是集版权图库及在线设计于一体的找图做图工具,一天是能下50张图片的,而且清晰度都是非常好的。该工具拥有20万+涵盖各行业可在线编辑的海报、营销长图等,深受用户们的信赖。https://zhidao.baidu.com/question/1456153315995330980.html
27.架设教程213=魔力强化(8) 228=快速施法(3) 229=快速魔力恢复(7) 285=强效魔力回复(27) X:L2Jcn-0822FWDl2jgameserverconfig内的options.properties用记事本打开把DefaultPunish = 1 设成1 就点怪左键+SHIFT 你可以看到 编辑页面 案编辑NPC 可以看到很多数值 NPC种类 就是设定他是商人.怪物.https://www.360doc.cn/article/3509002_57440247.html