欢迎阅读《学习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}}
(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
在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/
我们视图的函数体也已经修改为检查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/
在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[]:
IPython有一个很棒的功能,允许你打印关于对象的详细信息。这是通过输入对象的名称后跟一个问号()来完成的。内省Entry模型提供了一些信息,包括参数签名和表示该对象的字符串(称为docstring)的构造函数。
In[]:Entry#WhatisEntryandhowdowecreateitType:_BoundDeclarativeMetaStringForm:
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[]:[
In[]:oldest_to_newest=Entry.query.order_by(Entry.modified_timestamp.desc()).all()Out[]:[
In[]:Entry.query.filter(Entry.title=='Firstentry').all()Out[]:[
BinaryExpression只是一个表示逻辑比较的对象,并且是通过重写通常在Python中比较值时调用的标准方法而生成的。
为了检索单个条目,您有两个选项:.first()和.one()。它们的区别和相似之处总结在以下表中:
让我们尝试与之前相同的查询,但是,而不是调用.all(),我们将调用.first()来检索单个Entry实例:
In[]:Entry.query.filter(Entry.title=='Firstentry').first()Out[]:
在前面的示例中,我们测试了相等性,但还有许多其他类型的查找可能。在下表中,我们列出了一些您可能会发现有用的查找。完整列表可以在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'
现在我们既有博客条目模型,也有标签模型,我们需要一个第三个模型来存储两者之间的关系。当我们希望表示博客条目被标记为特定标签时,我们将在这个表中存储一个引用。以下是数据库表级别上正在发生的事情的图示:
由于我们永远不会直接访问这个中间表(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[]:[
要向条目添加标签,只需将它们分配给条目的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#Thepythonvariableisjustatag.Out[]:
本章最后要讨论的主题是如何对现有的模型定义进行修改。根据项目规范,我们希望能够保存博客条目的草稿。现在我们没有办法知道一个条目是否是草稿,所以我们需要添加一个列来存储条目的状态。不幸的是,虽然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代码:
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='
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模板并添加以下代码。如果看起来奇怪,不用担心。我们将逐行讲解。
Welcometomyblog
{%ifnumber%}Yournumberis{{number|int}}
- {%foriinrange(number|int)%}{%ifiisdivisibleby2%}
- {{i}} {%endif%}{%endfor%}
Nonumberspecified.
{%endif%}Yournameis{{name|default('
循环、控制结构和模板编程
让我们逐行讲解我们的新模板代码,从{%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('
在以下示例中,参数列表中的第一个参数将出现在管道符号的左侧。因此,即使我写了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/
为了访问这些新视图,我们需要使用我们的主要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('/
列出与给定标签匹配的条目将结合两个先前视图的逻辑。首先,我们需要使用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%}