为什么flask对flask response json的操作如此奇怪

体味流逝的每一个瞬间,珍惜幸福的每一段时光,记住曾经的每一次感动。
2017年九月
11121314151617
18192021222324
252627282930
最受欢迎的文章
- 37,585 views - 21,441 views - 20,241 views - 15,154 views - 11,912 views - 10,898 views - 10,355 views - 9,973 views - 9,828 views - 9,804 views{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"title":"chapter1 - 前期准备","author":"xujingwuwei","content":"使用虚拟环境在Python3.4及以上的版本中,已经内置了venv模块用以支持原生的虚拟环境,命令为pyvenv,并且已经在虚拟环境中内置了pip,所以强烈建议使用Python3.4及更新版本创建虚拟环境使用 pyvenv 文件夹名称即可创建一个虚拟环境,通常命名为venv。完整的示例命令为:pyvenv venv。即可在当前目录下创建一个名为venv的虚拟环境激活虚拟环境在Linux和Mac OS X系统中,可以通过以下命令激活虚拟环境:source venv/bin/activate激活之后虚拟环境的命令会修改命令提示符,加入环境名:(venv) $在虚拟环境中进行的任何python包的操作对系统中的python不会产生影响 如果要退出虚拟环境,可以在命令提示符下输入deactivate在虚拟环境中安装flask执行以下命令可在虚拟环境中安装Flask: (venv) $ pip install flask检查安装是否正确:(venv) $ python\n&&& import flask\n&&&\n没有错误提醒,即表明flask已在虚拟环境中正确安装!","updated":"T09:42:10.000Z","canComment":false,"commentPermission":"anyone","commentCount":2,"likeCount":4,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T17:42:10+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":2,"likesCount":4},"":{"title":"chapter2 - 程序的基本结构","author":"xujingwuwei","content":"2.1 初始化所有Flask程序都必须创建一个程序实例,程序实例是Flask类的对象:from flask import Flask\napp = Flask(__name__)\n2.2 路由和视图函数处理URL和函数之间关系的程序称为路由。在Flask中,使用程序实例提供的app.route修饰器,把修饰的函数注册为路由。如:@app.route('/')\ndef index():\n
return '&h1&Hello World!&/h1&'\n如上代码即把网站的根目录(用/表示)和index()函数绑定在一起,即访问网站首页时,会获得函数index()返回的内容。这里的index()被称为视图函数动态URL动态URL可以根据URL中参数的不同而返回不同的页面内容。在Flask中可以通过修饰器实现这一功能:@app.route('/user/&name&')\ndef user(name):\n
return '&h1&Hello, %s!&/h1&' % name\n尖括号中的内容就是动态部分。调用视图函数时,Flask会将动态部分作为参数传入函数。 路由中的动态部分默认使用字符串,不过也可以使用类型定义。例如,路由/user/&int:id&只会匹配动态片段id为整数的URL。Flask支持在路由中使用int、float、path类型,path类型也是字符串,但不把斜线视为分隔符,而将其当作动态片段的一部分。2.3 启动服务器程序实例用run方法来启动Flask中集成的Web服务器:if __name__ == '__main__':\n
app.run(debug=True)\n在run函数中设置debug=True可以启用调试模式,在服务器启动后,会进入轮询,直到程度停止2.4 一个完整的程序from flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n
return '&h1&Hello World!&/h1&'\n\nif __name__ == '__main__':\n
app.run(debug=True)\n将程序保存为hello.py,并使用(venv) $ python hello.py执行程序,可以看到在命令提示符中有类似于以下的提示:* Running on
(Press CTRL+C to quit)* Restarting with stat* Debugger is active!* Debugger pin code: 200-024-153此时,打开浏览器并在地址栏中输入即可以看到程序运行结果动态路由from flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n
return '&h1&Hello World!&/h1&'\n\n@app.route('/user/&name&')\ndef user(name):\n
return '&h1&Hello, %s!&/h1&' % name\n\nif __name__ == '__main__':\n
app.run(debug=True)\n此时,在服务器中访问即可以看到一个使用了name动态参数生成的欢迎消息。2.5 请求-响应循环2.5.1 程序和请求上下文将要处理的对象作为参数传入到视图函数中是一个很方便的处理方式,但这样会导致程序中的每个视图函数都需要增加一个参数。并且,如果视图函数需要处理多个对象时,情况会变得很糟糕。 Flask使用了上下文临时把某些对象变为全局可访问,来避免这一问题的出现。from flask import request\n\n@app.route('/')\ndef index():\n
user_agent = request.headers.get('Use-Agent')\n
return '&p&Your browser is %s&/p&' % user_agent\n如上代码中把request当作全局变量使用。运行后可在浏览器中看到当前使用的浏览器信息 Flask中存在着程序上下文和请求上下文两种上下文:变量名上下文说明current_app
程序上下文
当前激活程序的程序实例g
程序上下文处理请求时用作临时存储的对象。每次请求都会重设这个变量request
请求上下文请求对象,封装了客户端发出的HTTP请求中的内容session
请求上下文用户会话,用于存储请求之间需要“记住”的值的词典Flask在分发请求之前激活程序和请求上下文,请求处理完成后再将其删除。程序上下文使用示例&&& from hello import app\n&&& from flask import current_app\n&&& current_app.name\nTraceback (most recent call last):\n...\n&&& app_ctx = app.app_context()\n&&& app_ctx.push()\n&&& current_app.name\n'hello'\n&&& app_ctx.pop()\n在本例中,没激活上下文前调用current_app.name会导致错误,但在推送完上下文后就可以调用了2.5.2 请求调度请求调度用于处理URL和函数之间的关系,在这里称为URL映射。Flask使用app.route修饰器或者非修饰器形式的app.addurlrule()生成映射。 可以在虚拟环境中利用python shell检查hello.py生成的映射:(venv) $ python\n&&& from hello import app\n&&&& app.url_map\nMap([&Rule '/' (HEAD, OPTIONS, GET) -& index&,\n &Rule '/static/&filename&' (HEAD, OPTIONS, GET) -& static&,\n &Rule '/user/&name&' (HEAD, OPTIONS, GET) -& user&])\n/和/user/&name&路由在程序中使用app.route修饰器定义。/static/&filename&路由是Flask添加的特殊路由,用于访问静态文件 URL映射中的HEAD、OPTIONS、GET是请求方法,由路由进行处理。2.5.3 请求钩子请求钩子用于在处理请求之后或之后执行,可以避免在每个视图函数中都使用重复的代码。请求钩子用修饰器实现。Flask支持以下4种钩子钩子名称说明beforefirstrequest注册一个函数,在处理第一个请求之前运行before_request
注册一个函数,在每次请求之前运行after_request注册一个函数,如果没有未处理的异常抛出,在每次请求之后运行teardown_request注册一个函数,即使有未处理的异常抛出,也在每次请求之后运行2.5.4 响应响应即视图函数的返回值,通常是一个字符串。如果视图函数返回的响应需要使用不同的状态码,可以将数字代码作为第二个返回值。如下代码返回一个400状态码:@app.route('/')\ndef index():\n
return '&h1&Bad Request&/h1&', 400\nmake_response的使用Flask视图函数可以返回Response对象。make_response()函数可接受1个、2个或3个参数,并返回一个Response对象:from flask import make_response\n\n@app.route('/')\ndef index():\n
response = make_response('&h1&This document carries a cookie!&/h1&')\n
response.set_cookie('answer', '42')\n
return response\n重定向重定向是一种特殊的响应,可以使用302状态码表示,重定向指向的地址由Location首部提供。重定向响应可以使用3个值形式的返回值生成,也可以在Response对象中设定。但由于使用频繁,Flask提供了redirect()辅助函数:from flask import redirect\n\n@app.route('/')\ndef index():\n
return redirect('')\n错误处理处理错误用abort函数生成。如下例中,如果URL中动态参数id对应的用户不存在,则返回状态码404from flask import abort\n\n@app.route('/user/&id&')\ndef get_user(id):\n
user = load_user(id)\n
if not user:\n
abort(404)\n
return '&h1&Hello, %s&/h1&' % user.name\n要注意的是,abort抛出的异常是把控制权交给Web服务器而不是调用它的函数2.6 Flask拓展Flask被设计为可扩展形式,有很多功能需要用户自行选择安装或自行开发,例如数据库和用户认证。使用Flask-Script支持命令行选项使用Flask-Script扩展后,可以通过命令行参数的形式将参数传递给Flask的开发Web服务器 可通过以下命令安装该扩展:(venv) $ pip install flask-scriptFlask-Script扩展的使用示例 from flask import Flask\n from flask import make_response\n from flask.ext.script import Manager\n\n app = Flask(__name__)\n manager = Manager(app)\n\n @app.route('/')\n def index():\n
return '&h1&Hello World!&/h1&'\n\n @app.route('/user/&name&')\n def user(name):\n
return '&h1&Hello, %s!&/h1&' % name\n\n @app.route('/response')\n def response():\n
response = make_response('&h1&This document carries a cookie!&/h1&')\n
response.set_cookie('answer', '42')\n
return response\n\n if __name__ == '__main__':\n
manager.run()\n此时,执行python hello.py可得到一个使用提示。主要使用方法有两个:runserver和shell,前者用于启动Web服务器,后者用于启动python shell用于维护任务和调试。可分别通过以下命令执行:python hello.py runserverpython hello.py shell运行python hello.py runserver --help可获得更多帮助信息","updated":"T10:43:37.000Z","canComment":false,"commentPermission":"anyone","commentCount":9,"likeCount":3,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T18:43:37+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":9,"likesCount":3},"":{"title":"chapter3 - 使用模板","author":"xujingwuwei","content":"3.1 Jinja2模板引擎个人觉得模板主要用于前端的显示部分。模板中用到python语句的地方,需要用{% %}包围起来,并且有开始和结束两行,而变量名则需要用两个大括号包围:{{ }}。如下面为一个条件控制语句:{% if user %}\n
Hello, {{ user }}!\n{% else %}\n
Hello, Stranger!\n{% endif %}\n模板中需要注释掉部分语句时,也需要用一对大括号内加一对#号包围,如:{# for message in messages #}\n上面代码中的for循环将被注释掉而不会被执行 3.1.1 渲染模板Jinja2是Flask默认使用的模板引擎。默认情况下,Flask程序在程序文件夹中的templates子文件夹中寻找模板。简单的模板使用示例:from flask import Flask\nfrom flask import render_template\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n
return render_template('index.html')\n\n@app.route('/user/&name&')\ndef user(name):\n
return render_template('user.html', name=name)\n\nif __name__ == '__main__':\n
manager.run()\n如上,render_template函数的第一个参数是模板的文件名,后面的参数都是键值对,表示模板文件中接收的变量。如上的user()函数中,将会把name传给user.html文件。最简单的user.html文件(位置为templates/user.html)可以是:&h1&Hello, {{ name }}!&/h1&\n由此,通过user()函数传入的name参数将会在模板中的相应位置显示3.1.2 变量Jinja2可以识别所有类型的变量,包括列表、字典、对象。也可以使用过滤器修改变量,变量名和过滤器之间使用竖线分隔。如下述模板以首字母大写形式显示变量name的值:Hello, {{ name|capitalize }}\n以下是Jinja2提供的部分常用的过滤器:\n\n过滤器名\n说明\nsafe\n渲染值时不转义\ncapitalize\n把值的首字母转换成大写,其它字母转换成小写\nlower\n把值转换成小写形式\nupper\n把值转换成大写形式\ntitle\n把值中每个单词的首字母都转换成大写\ntrim\n把值的首尾空格去掉\nstriptags\n渲染之前把值中所有的HTML标签都删掉\n值得注意的是safe过滤器。默认情况下,Jinja2会转义所有变量。如果把 '&h1&Hello&/h1&'转换成'&h1&Hello&/h1&',如果需要显示变量中存储的HTML代码,就可以使用safe过滤器,但千万不要在不可信的值上使用safe过滤器。\n完整的过滤器列表在:3.1.3 控制结构Jinja2的控制结构与一般编程语言的控制语句有点类似。条件控制语句{% if user %}\n
Hello, {{ user }}!\n{% else %}\n
Hello, Stranger!\n{% endif %}\n渲染一组元素&ul&\n
{% for comment in comments %}\n
&li&{{ comment }}&/li&\n
{% endfor %}\n&/ul&\n使用宏# 定义宏\n{% macro render_comment(comment) %}\n
&li&{{ comment }}&/li&\n{% endmacro %}\n# 使用宏\n&ul&\n
{% for comment in comments %}\n
{{ render_comment(comment) }}\n
{% endfor %}\n&/ul&\n为了方便重复使用,还可以将宏保存在单独的文件中,在需要使用时再在模板中导入:{% import 'macros.html' as macros %}\n&ul&\n
{% for comment in comments %}\n
{{ macros.render_comment(comment) }}\n
{% endfor %}\n&/ul&\n还可以将其写入单独的文件中,以避免重复:{% include 'common.html' %}\n模板继承模板继承类似于Python中的类继承。先写一个名为base.html的基模板:&html&\n&head&\n
{% block head %}\n
&title&{% block title %}{% endblock %} - My Application&/title&\n
{% endblock %}\n&body&\n
{% block body %}\n
{% endblock %}\n&/body&\n&/head&\n&/html&\n如上,在block标签中的元素是可以在衍生模板中修改的,如:{% extends \"base.html\" %}\n{% block title %}Index{% endblock %}\n{% block head %}\n
{{ super() }}\n
&/style&\n{% endblock %}\n{% block body %}\n&h1&Hello, World!&/h1&\n{% endblock %}\nextends指令声明这个模板衍生自base.html。其后基模板中的3个块title、head、body被重新定义。注意在新定义的head块中,因为在基模板中其内容不是空的,所以要使用super()来获取原来的内容模板中的所有block都必须要有结束的block语句3.2 使用Flask-Bootstrap集成Twitter BootstrapBootstrap是一个前端的js框架,可以用创建优雅的UI。可以使用pip安装:(venv) $ pip install flask-bootstrap\n\n初始化Flask-Bootstrap\n\n在代码中加入:# 原书中的代码是 from flask.ext.bootstrap import Bootstrap\n# 但是我在python3.5中运行时,被提示说这种用法已经过时了\n# 而应该使用下面这种导入方法\nfrom flask_bootstrap import Bootstrap\n#...\n# 下面这行代码应在app = Flask(__name__)之后\nbootstrap = Bootstrap(app)\n#...\n在模板中使用Flask-Bootstrap修改templates/base.html文件:{% extends \"bootstrap/base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block navbar %}\n&div class=\"navbar navbar-invrese\" role=\"navigation\"&\n
&div class=\"container\"&\n
&div class=\"navbar-header\"&\n
&button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\"&\n
&span class=\"sr-only\"&Toggle navigation&/span&\n
&span class=\"icon-bar\"&&/span&\n
&span class=\"icon-bar\"&&/span&\n
&span class=\"icon-bar\"&&/span&\n
&/button&\n
&a class=\"navbar-brand\" href=\"/\"&Flasky&/a&\n
&div class=\"navbar-collapse collapse\"&\n
&ul class=\"nav navbar-nav\"&\n
&li&&a href=\"/\"&Home&/a&&/li&\n
&/div&\n&/div&\n{% endblock %}\n\n{% block content %}\n&div class=\"container\"&\n
&div class=\"page-header\"&\n
&h1&Hello, {{ name }}!&/h1&\n
&/div&\n&/div&\n{% endblock %}\n以上模板中,定义了3个块,分别是title、navbar、content,这些块都是基模板提供的,可以在这里重新定义。其中navbar和content两个块分别表示页面中的导航条的主体内容。\n更多的Bootstrap信息可参考。Flask-Bootstrap的base.html模板还定义了很多其它块,都可以在衍生模板中使用:\n\n块名\n说明\ndoc\n整个HTML文档\nhtml_attribs\n&html&标签中的属性\nhtml\n&html&标签中的内容\nhead\n&head&标签中的内容\ntitle\n&title&标签中的内容\nmetas\n一组&meta&标签\nstyles\n层叠样式表CSS定义\nbody_attribs\n&body&标签的属性\nbody\n&body&标签中的内容\nnavbar\n用户定义的导航条\ncontent\n用户定义的页面内容\nscripts\n文档底部的JavaScript声明\n但应注意的是,上表中的很多块都是Flask-Bootstrap自用的,直接重定义可能会出现问题,例如,Bootstrap所需的文件在sytles和scripts块中声明,如果需要添加新内容,则必须使用Jinja2的super()函数,例如:{% block scripts %}\n{{ super() }}\n&script type=\"text/javascript\" src=\"my-script.js\"&/script&\n{% endblock %}}\n这样就不会覆盖掉基模板中的原有内容3.3 自定义错误页面在没有定义错误页面之前,如果访问一个不存在的URL,浏览器会显示一个它内置的404页面。Flask提供了errorhandler()修饰器,允许程序自定义错误页面。如下两段代码可以指定特定的404和500错误页面:@app.errorhandler(404)\ndef page_not_found(e):\n
return render_template('404.html'), 404\n\n@app.errorhandler(500)\ndef internal_server_error(e):\n
return render_template('500.html'), 500\n为了方便使用,作者在此处把前面定义好的user.html模板改写作为项目里面的基模板base.html,然后再新建user.html模板继承自base.html。基模板如下:{% extends \"bootstrap/base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block navbar %}\n&div class=\"navbar navbar-inverse\" role=\"navigation\"&\n
&div class=\"container\"&\n
&div class=\"navbar-header\"&\n
&button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\"&\n
&span class=\"sr-only\"&Toggle navigation&/span&\n
&span class=\"icon-bar\"&&/span&\n
&span class=\"icon-bar\"&&/span&\n
&span class=\"icon-bar\"&&/span&\n
&/button&\n
&a class=\"navbar-brand\" href=\"/\"&Flasky&/a&\n
&div class=\"navbar-collapse collapse\"&\n
&ul class=\"nav navbar-nav\"&\n
&li&&a href=\"/\"&Home&/a&&/li&\n
&/div&\n&/div&\n{% endblock %}\n\n{% block content %}\n&div class=\"container\"&\n
{% block page_content %}{% endblock %}\n&/div&\n{% endblock %}\n基模板主要定义了默认标题和导航栏内容,内容部分content block只放了一个div,其中包含了一个名为page_content的空白块,块中的内容由衍生模板定义,作为页面主体内容显示。\n由此重新组织的templates/user.html页面为:{% extends \"base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block page_content %}\n&div class=\"page-header\"&\n
&h1&Hello, {{ name }}!&/h1&\n&/div&\n{% endblock %}\n404错误页面templates/404.html{% extends \"base.html\" %}\n\n{% block title %}Flasky - Page Not Found{% endblock %}\n\n{% block page_content %}\n&div class=\"page-header\"&\n
&h1&Not Found&/h1&\n&/div&\n{% endblock %} 详细内容可参考","updated":"T11:00:20.000Z","canComment":false,"commentPermission":"anyone","commentCount":0,"likeCount":2,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T19:00:20+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":0,"likesCount":2},"":{"title":"chapter4 - Web表单","author":"xujingwuwei","content":"本书使用的是Flask-WTf扩展来处理Web表单,它是把一个独立的集成了在Flask程序中。可以使用pip安装:(venv) $ pip install flask-wtf4.1 跨站请求伪造保护默认情况下,Flask-WTF可以保护所有表单免受跨站请求伪造(Cross-Site Request Forgery, CSRF)的攻击。为了实现这个防护,Flask-WTf需要在程序中设置一个密钥,然后再使用这个密钥生成加密令牌,再用令牌验证请求中表单数据的真伪。设置密钥的方法如下: hello.pyapp = Flask(__name__)\napp.config['SECRET_KEY'] = 'your secret key'\n为了安全起见,密钥是不应该硬编码到程序中的,而是要保存到环境变量中。到第7章会介绍4.2 表单类使用Flask-WTF时,每个web表单都继续自Form类。这个类中包含了常用HTML表单中的各种字段,还为这些字段设置了一些验证函数可供使用,用于验证表单的输入数据。 如下为一个简单的weg表单,包含了一个文本字段和一个提交按钮: hello.pyfrom flask_wtf import Form\nfrom wtforms import StringField, SubmitField\nfrom wtforms.validators import Required\n\nclass NameForm(Form):\n
name = StringField('What is your name?', validators=[Required()])\n
submit = SubmitField('Submit')\nname是一个文本字段,submit是一个提交按钮,分别相当于HTML中的:&input type=\"text\"&\n&input type=\"submit\" value=&\nname使用的StringField方法的第一个参数是显示在页面中的提示信息,第二个参数是一个表单验证项,用validators=[]方法实现,该方法接收一个列表,可在列表中放置多个验证函数。 以下是WTForms支持的HTML标准字段:字段类型说明StringField文本字段TextAreaField多行文本字段PasswordField密码文本字段HiddenField隐藏文本字段DateField文本字段,值为datetime.date格式DateTimeField文本字段,值为datetime.datetime格式IntegerField文本字段,值为整数DecimalField文本字段,值为decimal.DecimalFloatField文本字段,值为浮点数BooleanField复选框,值为True或FalseRadioField一组单选框SelectField下拉列表SelectMultipleField下拉列表,可选择多个值FileField文件上传字段SubmitField表单提交按钮FormField把表单作为字段嵌入另一个表单FieldList一组指定类型的字段以下是WTForms内建的验证函数:验证函数说明Email验证电子邮件地址EqualTo比较两个字段的值;常用于密码确认IPAddress验证IPv4网络地址Length验证输入字符串的长度NumberRange验证输入的值在数字范围内Optional无输入值时跳过其他验证函数Required确保字段中有数据Regexp使用正则表达式验证输入值URL验证URLAnyOf确保输入值在可选值列表中NoneOf确保输入值不在可选值列表中以下是几个字段和验证函数的使用示例,完整的使用示例和说明可参考末尾处提供的文档。密码表单字段的使用:from wtforms import PasswordField\n\npwd1 = PasswordField('请输入你的密码:', validators=[Required()])\npwd2 = PasswordField('确认密码:', validators=[Required(), EqualTo('pwd1', message=\"密码不匹配\")])\n首先要导入字段函数,不过为了方便整个表单的使用,也可以直接使用from wtforms.fields import *的方式导入所有可用表单字段。PasswordField()方法的第一个参数是提示信息,第二个参数是验证函数,值是由验证函数组成的列表,用Required()可以设置必填项,EqualTo()用于匹配两个表单的输入,EqualTo()函数的第一个参数是需要进行比对的字段名称,第二个参数是匹配失败的时候的提示信息单选按钮的使用from wtforms import RadioField\n\nradios = RadioField('sex', choices=[(1, 'one'), (2, 'two')])\n与其它字段类似,第一个参数是名字,但目前还没有发现有什么用处,在前端也不显示。第二个参数就是单选按钮的各个选项,可以是元组类型组成的列表,元组的第一个参数是value的值,第二个参数是显示在页面前端的名字。如上代码在前端中是这样子的:&div class=\"radio\"&\n
&input id=\"radio-0\" name=\"radio\" value=\"1\" type=\"radio\"& one\n
&/label&\n&/div&\n&div class=\"radio\"&\n
&input id=\"radio-1\" name=\"radio\" value=\"2\" type=\"radio\"& two\n
&/label&\n&/div&\n对,Jinja2默认地用两个div来分别显示这两个radio了,默认地在页面前端看到的就是一个在上一个在下多选框的使用from wtforms import BooleanField\n\ncheckbox = BooleanField('one', default=True)\n第一个参数是显示在页面的名字,第二个参数可设置是否默认选中,如果不设置或者为False,则不选中4.3 把表单渲染成HTML表单字段是可调用的,可以通过参数例如form传到模板中,然后可以在模板中生成一个简单的表单:&form method=\"POST\"&\n
{{ form.hidden_tag() }}\n
{{ form.name.label }} {{ form.name() }}\n
{{ form.submit() }}\n&/form&\n还可以把参数传入渲染字段的函数来改进表单的外观,传入的参数会被转换成字段的HTML属性。如下代码可以指定name字段的id属性:&form method=\"POST\"&\n
{{ form.hidden_tag() }}\n
{{ form.name.label }} {{ form.name(id='my-text-field') }}\n
{{ form.submit() }}\n&/form&\n当然其实Bootstrap已经为我们提供了一些表单样式,可以这样调用:{% import \"bootstrap/wtf.html\" as wtf %}\n{{ wtf.quick_form(form) }}\n于是完整的表单代码如下: 修改 templates/index.html{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block page_content %}\n&div class=\"page-header\"&\n
&h1&Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!&/h1&\n&/div&\n{{ wtf.quick_form(form) }}\n{% endblock %}\n4.4 在视图函数中处理表单目前为止,表单在视图函数中定义了,而模板中也对表单数据进行了渲染,还没有把数据发送到模板,因此需要修改视图函数: 修改 hello.py@app.route('/', methods=['GET', 'POST'])\ndef index():\n
name = None\n
form = NameForm()\n
if form.validate_on_submit():\n
name = form.name.data\n
form.name.data = ''\n
return render_template('index.html', form=form, name=name)\n在app.route修饰器中添加了method参数,使得index()视图函数可以处理GET和POST请求,如果没有添加的话,则只能处理GET请求,而表单提交通常应使用POST请求。 由于在表单类中为name字段设置了验证函数,所以如果验证通过,则validate_on_submit()方法会返回True,否则返回False。首次加载页面时,由于表单中没有数据,因此会返回False,直接加载表单,而在填写了表单数据并验证难过后,服务器会收到一个POST请求,就可以把输入的数据赋值给name变量并传到模板中进行渲染。form.name.data = ''可以在获取了表单输入数据后清空输入框。 而如果验证没通过的话,点击提交按钮时会收到一个错误提醒。4.5 重定向和用户会话在当前版本的hello.py中,如果在输入名字后提交表单,再刷新时,会被提示是否要重新发送已填写数据。在多数情况下,这并不是个理想的处理方式。 使用重定向就可以很好地解决这个问题。Flask中把重定向当成一种特定的响应,其响应的内容是URL。但还有一个问题,就是当程序处理POST请求结束后,表单中获取到的用户输入数据就丢失了,如果使用重定向,就不能继续使用这些数据。此时,就要使用到用户会话,也就是第2章中提到的session。使用了重定向和用户会话后的视图函数如下所示: hello.pyfrom flask import Flask, render_template, session, redirect, url_for\n\n@app.route('/', methods=['GET', 'POST'])\ndef index():\n
form = NameForm()\n
if form.validate_on_submit():\n
session['name'] = form.name.data\n
return redirect(url_for('index'))\n
return render_template('index.html', form=form, name=session.get('name'))\n本程序使用了用户会话session来替代了前面的局部变量name,所以在两次请求之间也能够记住输入的数据。 如上代码中,如果表单输入正确,则会调用到redirect()函数。该函数用于生成HTTP重定向响应,其参数是重定向的URL,它的第一个参数必须指定端点名,即路由内部的名字,默认情况下路由端点是相应视图函数的名字。4.6 Flash消息Flash消息用于显示确认消息、警告或者错误提醒。来自Flask文档中的Flash消息闪现说明:闪现系统的基本工作方式是:在且只在下一个请求中访问上一个请求结束时记录的消息。也就是说,当前看到的Flash消息,是上一次请求结束后设置的。在hello.py文件中使用Flash消息:from flask import redirect\nfrom flask import flash\nfrom flask import url_for\n\n@app.route('/' methods=['GET', 'POST'])\ndef index():\n
form = NameForm()\n
if form.validate_on_submit():\n
old_name = session.get('name')\n
if old_name is not None and old_name != form.name.data:\n
flash('Looks like you have changed your name!')\n
session['name'] = form.name.data\n
return redirect(url_for('index'))\n
return render_template('index.html', form=form, name=session.get('name'))\n在示例中,每次提交的名字会跟存储在session中的上一次提交的名字做比较,如果不一样,则会调用flash()函数,在发给客户端的下一个响应中显示消息。flash()函数返回的消息需要被模板接收渲染:修改 templates/base.html{% block content %}\n&div class=\"container\"&\n
{% for message in get_flashed_messages() %}\n
&div class=\"alert alert-warning\"&\n
&button type=\"button\" class=\"close\" data-dismiss=\"alert\"&&&/button&\n
{{ message }}\n
{% endfor %}\n
{% block page_content %}{% endblock %}\n&/div&\n{% endblock %}\n在模板中使用循环是因为在之前的请求循环中每次调用flash()函数时,都会生成一个消息,所以可能会有多个消息在排除等待显示。get_flashed_message()函数获取的消息在下次调用时不会再次返回,其实这个函数获取到的消息是一个列表。WTForms表单的具体使用可参数官方文档Flash消息闪现的使用可参","updated":"T11:16:42.000Z","canComment":false,"commentPermission":"anyone","commentCount":0,"likeCount":2,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T19:16:42+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/v2-fef33d9fbd_r.png","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":0,"likesCount":2},"":{"title":"chapter5 - Web数据库","author":"xujingwuwei","content":"5.1 Python数据库框架Flask允许自己选择需要的数据库框架,但在选择时,应考虑这些因素:易用性 这里要注重的两个概念是ORM或ODM,也就是对象关系映射和对象文档映射。它们用于把高层的面向对象操作转换成低层的数据库指令。性能可移植性 是否可在多个平台中平移Flask集成度 使用集成了Flask的框架可以简化配置和操作因此本书选择的是,这个Flask扩展包装了框架5.2 使用Flask-SQLAlchemy管理数据库安装可使用pip安装:(venv) $ pip install flask-sqlalchemy。在Flask-SQLAlchemy中,数据库使用URL指定,也就是以URL的形式来连接数据库,而不是sql原生的connect之类的操作。具体格式如下表:\n数据库引擎\nURL\nMySQL\nmysql://username:password@hostname/database\nPostgres\npostgresql://user\nSQLite (Unix)\nsqlite:////absolute/path/to/database\nSQLite (Windows)\nsqlite:///c:/absolute/path/to/database\nhostname表示MySQL服务所在的主机,可以是本地主机或远程服务器 database表示要使用的数据库名称 username和password表示需要有到的数据库用户密令本书使用的是SQLite数据库,它不需要服务器,因此URL中的database是硬盘上的文件名配置数据库程序使用的数据库URL必须保存到Flask配置对象的SQLALCHEMY_DATABASE_URI中,另外还有个要设置的是SQLALCHEMY_COMMIT_ON_TEARDOWN键,将其设为True时,每次执行SQL请求后会自动提交。经个人实测发现,如果仅做了以上两个设置,在运行时会有警告:/home/cavin/Code/Python/flask/lib64/python3.5/site-packages/flask_sqlalchemy/__init__.py:800: UserWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.
Set it to True to suppress this warning.\n
warnings.warn('SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future.
Set it to True to suppress this warning.')\n按提示说的,把这个变量配置成True即可消除这个警告:app.config['SQLACHEMY_TRACK_MODIFICANTS'] = True\n因此,配置后的hello.py文件为:from flask_sqlalchemy import SQLAlchemy\n\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\napp = Flask(__name__)\napp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')\napp.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True\napp.config['SQLALCHEMY_TRACK_MODIFICANTS'] = True\n\ndb = SQLAlchemy(app)\n注意以上(除了app = Flask(__name__))外都是新添加到文件中的内容,其它内容不变basedir变量可以获取当前文件的绝对路径。 db是SQLAlchemy类的一个实例,表示程序使用的数据库,同时也获得了Flask-SQLAlchemy提供的所有功能。5.3 定义模型其实这里所说到的“模型”,在某种程度上可看做是传统SQL中的“数据表”。如下可定义Role和User模型:class Role(db.Model):\n
__tablename__ = 'roles'\n
id = db.Column(db.Integer, primary_key=True)\n
name = db.Column(db.String(64), unique=True)\n\n
def __repr__(self):\n
return '&Role %r&' % self.name\n\nclass User(db.Model):\n
__tablename__ = 'users'\n
id = db.Column(db.Integer, primary_key = True)\n
username = db.Column(db.String(64), unique=True, index=True)\n\n
def __repr__(self):\n
return '&User %r&' % self.username\n其中,__tablename__定义了在数据库中使用的表名。其余的id、name、username这些都是模型的属性,相当于数据表中的字段,用db.Column构造函数实现,其第一个参数是数据库列和模型属性的类型。模型中的__repr()__方法并不是强制要求的,这里定义了并返回一个具有可读性的字符串表示模型,可方便调试和测试。 常用的SQLAlchemy列类型如下:\n类型名\nPython类型\n说明\nInteger\nint\n普通整数,一般是32位\nSmallInteger\nint\n取值范围小的整数,一般是16位\nBigInteger\nint或long\n不限制精度的整数\nFloat\nfloat\n浮点数\nNumeric\ndecimal.Decimal\n定点数\nString\nstr\n变长字符串\nText\nstr\n变长字符串,对较长或不限长度的字符串做了优化\nUnicode\nunicode\n变长Unicode字符串\nUnicodeText\nunicode\n变长Unicode字符串,对较长或不限长度的字符串做了优化\nBoolean\nbool\n布尔值\nDate\ndatetime.date\n日期\nTime\ndatetime.time\n时间\nDateTime\ndatetime.datetime\n日期和时间\nInterval\ndatetime.timedelta\n时间间隔\nEnum\nstr\n一组字符串\nPickleType\n任何Python对象\n自动使用Pickle序列化\nLargeBinary\nstr\n二进制文件\ndb.Column中其余的参数指定属性的配置选项,常用的可选项如下:\n选项名\n说明\nprimary_key\n如果设置为True,这列就是表的主键\nunique\n如果设置为True,这列不允许出现复生值\nindex\n如果设置为True,为这列创建索引\nnullable\n如果设置为True,这列允许为空;为False则不允许空\ndefault\n为这列定义默认值\nFlask-SQLAlchemy要求每个模型都要定义主键,通常命名为id5.4 关系关系也即不同表之间的联系。例如,一个论坛中的用户有其自己的id,其归属于一个分组,这个组也有组的id,用户信息表和分组表可以通这两个各自的id来关联起来。一个组可以有多个用户,但一个用户同时只能归属于一个组。这就是一对多的关系。 在模型中表示如下: 修改hello.pyclass Role(db.Model):\n
# 之前的内容\n
users = db.relationship('User', backref='role')\n\nclass User(db.Model):\n
# 之前的内容\n
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))\nUser模型中的role_id表被定义为外键,建立起了关系,传给db.ForeignKey()的参数roles.id表明,这列的值是roles表中的id值。 在Role模型中,users属性返回与角色相关联的用户组成的列表,db.relationship()的第一个参数表明这个关系连接的是哪一个表。第二个参数backref向User模型添加了一个role属性,定义了反向关系。 定义关系时(db.relationship())常用的配置选项有:\n选项名\n说明\nbackref\n在关系的另一个模型中添加反向引用\nprimaryjoin\n明确指定两个模型之间使用的联结条件\nlazy\n指定如何加载相关记录。可选值有select-首次访问时按需加载;immediate-源对象加载后就加载;joined-加载记录,但使用联结;subquery-立即加载,但使用子查询;noload-永不加载、dynamic-不加载记录,但提供加载记录的查询\nuselist\n如果设为False,不使用列表,而使用标量值\norder_by\n指定关系中记录的排序方式\nsecondary\n指定多对多关系中关系表的名字\nsecondaryjoin\nSQLAlchemy无法自行决定时,指定多对多关系中的二级联结条件\n5.5 数据库操作5.5.1 创建表(venv) $ python hello.py shell\n&&& from hello import db\n&&& db.create_all()\n此时,就可在程序目录中新建了一个名为data.sqlite的文件。5.5.2 插入数据&&& from hello import Role, User\n&&& admin_role = Role(name='Admin')\n&&& mod_role = Role(name='Moderator')\n&&& user_role = Role(name='User')\n&&& user_john = User(username='john', role=admin_role)\n&&& user_susan = User(username='susan', role=user_role)\n&&& user_david = User(username='david', role=user_role)\n此时,这些对象的id属性并没有被明确设定,因为主键是Flask-SQLAlchemy管理的。而且这些对象目前也只存在于Python中,并没有写入到数据库。可以通过数据库会话来管理这些改动。在Flask-SQLAlchemy中,会话由db.session表示。在写入数据库前,要先把这些对象添加到会话中:&&& db.session.add(admin_role)\n&&& db.session.add(mod_role)\n&&& db.session.add(user_role)\n&&& db.session.add(user_john)\n&&& db.session.add(user_susan)\n&&& db.session.add(user_david)\n或简写方式:&&& db.session.add_all([admin_role, mod_role, user_role, user_john, user_susan, user_david])\n写入数据库:&&& mit()\n应注意的是,这里的数据库会话db.session和第4章中提到的Flask session对象没有关系,它只是数据库会话,或称为事务。数据库会话可以保持数据库操作的一致性。 当然,它也有回滚:db.session.rollback()5.5.3 修改行继续在前面的shell会话中操作。把Admin角色重命名为Administrator&&& admin_role.name = 'Administrator'\n&&& db.session.add(admin_role)\n&&& mit()\n5.5.4 删除行如下可删除Moderator&&& db.session.delete(mod_role)\n&&& mit()\n5.5.5 查询行Flask-SQLAlchemy为每个模型类都提供了query对象。查询所有记录 相当于原生SQL语句的select * from&&& Role.query.all()\n[&Role u'Administrator'&, &Role u'User'&]\n使用过滤器 相当于原生SQL语句中的where 查找角色为User的所有用户:&&& User.query.filter_by(role=user_role).all()\n[&User u'susan'&, &User u'david'&]/* 日,更新数据库迁移说明 */5.6 使用Flask-Migrate实现数据库迁移所谓数据库迁移,其实主要是数据库(包含其结构与字段属性等)的更新。比如说新增了表、字段等。使用Flask-Migrate可以很好地实现数据库平移更新,不改变原有结构和数据,也不需要删除重建。5.6.1 创建迁移仓库先要安装Flask-Migrate:(venv) $ pip install flask-migrate在程序入口文件hello.py中增加对数据库迁移的支持:from flask_migrate import Migrate, MigrateCommand\n\n# .其它原有代码\n\nmigrate = Migrate(app, db)\nmanager.add_command('db', MigrateCommand)\n\n# 其它代码然后才是使用init子命令来创建迁移仓库:(venv) $ python hello.py db init这个命令会创建migrations文件夹,所有的迁移脚本都会放在里面5.6.2 创建迁移脚本(venv) $ python hello.py db migrate -m \"initial migration\"5.6.3 更新数据库(venv) $ python hello.py db update这里应该要提出的是,在本书后面,作者在需要进行更新数据库的时候,只会提到需要读者执行更新数据库的命令:python hello.py update,却没有说到需要执行创建迁移脚本命令。这是因为作者默认读者是从本书的github仓库上pull代码的,而pull下来的代码里面已经包含有数据库迁移脚本了,所以不需要再创建。如果是自己更新代码而不是从github上下载代码的话,那么就必须要先执行创建迁移脚本的命令,然后再执行更新数据库的命令才会有效。切记!","updated":"T02:14:42.000Z","canComment":false,"commentPermission":"anyone","commentCount":3,"likeCount":2,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T10:14:42+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":3,"likesCount":2},"":{"title":"chapter6 - 电子邮件","author":"xujingwuwei","content":"在很多程序中都需要使用电子邮件,用于用户验证、信息提醒等。Python标准库中的smtplib包可用于在Flask程序中发送邮件,而Flask-Mail扩展则对其进行了包装,使其更适用于Flask的开发。安装及配置\n安装\n使用pip安装:(venv) $ pip install flask-mail\n\n配置SMTP服务器信息\n要使用SMTP服务器进行邮件发送,还需要对其进行配置,如服务器地址、端口,还需要用于登录邮件服务器的用户名和密码。以下是可用的SMTP服务器配置:\n配置\n默认值\n说明\nMAIL_SERVER\nlocalhost\n电子邮件服务器的主机名或IP地址\nMAIL_PORT\n25\n电子邮件服务器的端口\nMAILUSETLS\nFalse\n启用传输层安全TLS协议\nMAILUSESSL\nFalse\n启用安全套接层SSL协议\nMAIL_USENAME\nNone\n邮件账户的用户名\nMAIL_PASSWORD\nNone\n邮件账户的密码\nhello.py:配置Flask-Mail使用新浪邮箱 原书中使用的是Gmail,在国内使用不便,因此配置为新浪邮箱import os\n# 原有内容\napp.config['MAIL_SERVER'] = ''\napp.config['MAIL_PORT'] = 25'\napp.config['MAIL_USE_TLS'] = True\napp.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')\napp.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')\n为了安全起见,自己的账户信息是不应该写在脚本中的。以上代码中最后两行即设置了从环境变量中获取邮件的帐户名和密码。\n初始化Flask-Mail\nfrom flask_mail import Mail\nmail = Mail(app)\n\n在环境变量中设置邮件用户名和密码\nLinux/Mac OS X(venv) $ export MAIL_USERNAME='your username'\n(venv) $ export MAIL_PASSWORD='your password'\nWindows(venv) $ set MAIL_USERNAME='your username'\n(venv) $ set MAIL_PASSWORD='your password'\n在Python shell中发送电子邮件(venv) $ python hello.py shell\n&&& from flask_mail import Message\n&&& from hello import mail\n&&& msg = Message('test subject', sender='', recipients=[''])\n&&& msg.body = 'text body'\n&&& msg.html = '&b&HTML&/b& body'\n&&& with app.app_context():\n...
mail.send(msg)\n...\nMessage()函数设置了邮件信息,以上代码中的三个参数分别为:邮件主题、发件人邮箱(应当是前面在环境变量中设置好的MAIL_USERNAME)、收件人列表。body和html则分别代表邮件内容和HTML格式的内容。最后使用send()方法执行发送操作。Flask-Mail中的send()函数使用current_app,因此需要在激活的程序上下文中执行。在程序中集成发送电子邮件功能在hello.py中添加电子邮件支持\n邮件发送函数\nfrom flask_mail import Message\n\napp.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'\napp.config['FLASKY_MAIL_SENDER'] = ''\n\ndef send_mail(to, subject, template, **kwargs):\n
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])\n
msg.body = render_template(template + '.txt', **kwargs)\n
msg.html = render_template(template + '.html', **kwargs)\n
mail.send(msg)\n以上的是系统管理员邮箱,用于发送邮件的,而[Flasky]则用于主题前缀。send_mail()程序中使用模板时不能包含扩展名,这样才便于使用两个模板分别渲染纯文本邮件和HTML邮件。\n视图函数中的设置\n如果提交的用户是新用户,则给管理员发送一封邮件提醒# 原有内容\napp.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')\n# 原有内容\n@app.route('/', methods=['GET', 'POST'])\ndef index():\n
form = NameForm()\n
if form.validate_on_submit():\n
user = User.query.filter_by(username=form.name.data).first()\n
if user is None:\n
user = User(username=form.name.data)\n
db.session.add(user)\n
session['known'] = False\n
if app.config['FLASKY_ADMIN']:\n
send_mail(app.config['FLASKY_ADMIN'], 'New User', 'mail/new_user',
user=user)\n
session['known'] = True\n
session['name'] = form.name.data\n
return redirect(url_for('index'))\n
return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False))\n为了正确执行程序,需要在templates文件夹下创建mail子文件夹,用于放置邮件模板。在其中新建两个邮件模板,模板内容其实就是邮件内容了,很简单,可以自定义:templates/mail/new_user.txtUser {{ user.username }} has joined.\ntemplates/mail/new_user.htmlUser &b&{{ user.username }}&/b& has joined.\n这里的user是否前面的渲染模板语句中的**kwargs传入的。程序中用到了环境变量FLASKY_ADMIN,可如下设置:Linux/Mac OS X(venv) $ export FLASKY_ADMIN='your-email-address'\nWindows(venv) $ set FLASKY_ADMIN='your-email-address'\n此时,就可以进行测试了,当在表单中写入新名字提交时,管理员就会收到一封邮件提醒。异步发送电子邮件当在发送电子邮件时,可发现程序有停滞现象,表现在浏览器上就是刷新按钮在转,这时就是send_mail()函数正在执行。为了避免这种不必要的延迟,可以使用异步的方法执行邮件发送函数。 修改hello.py:from threading import Thread\n\ndef send_async_email(app, msg):\n
with app.app_context():\n
mail.send(msg)\n\ndef send_mail(to, subject, template, **kwargs):\n
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])\n
msg.body = render_template(template + '.txt', **kwargs)\n
msg.html = render_template(template + '.html', **kwargs)\n
thr = Thread(target=send_async_email, args=[app, msg])\n
thr.start()\n
return thr\n到此,程序中便中成功集成了电子邮件的功能。但是如果在要程序中发送大量电子邮件时,使用专门发送电子邮件的作业要比给每封邮件都新建一个线程更合适。例如,可以把执行send_async_email()函数的操作发给任务队列。","updated":"T03:38:11.000Z","canComment":false,"commentPermission":"anyone","commentCount":2,"likeCount":3,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T11:38:11+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":2,"likesCount":3},"":{"title":"chapter7 - 大型程序的结构","author":"xujingwuwei","content":"在程序体积变得越来越大的时候,如果还是在单一脚本中编写,就会显得很不方便。于是需要对程序进行结构上的拆分。Flask并不强制要求使用特定的组织方式,开发者可以自行决定。7.1 项目结构|-flasky\n|-app/\n|-__init__.py\n|-email.py\n|-models.py\n|-templates/\n|-static/\n|-main/\n|-__init__.py\n|-errors.py\n|-forms.py\n|-views.py\n|-migrations/\n|-tests/\n|-__init__.py\n|-test*.py\n|-venv\n|-requirements.txt\n|-config.py\n|-manage.py\n我们要关心的是以下3个文件夹:Flask程序保存在名为app的包中migrations文件夹包含数据库迁移脚本tests包里面是单元测试脚本还有一些新文件:requirements.txt 该文件列出了所有信赖包,方便在其它电脑中生成同样的需求环境config.py 配置文件manage.py 用于启动程序以及其它的程序任务7.2 配置选项本例中设置了开发、测试、部署这三种不同环境下的配置。并设置了一个默认的配置:开发环境config.py 配置文件import os\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\nclass Config:\n
SECRET_KEY = os.environ.get('SECRET_KEY') or 'your string'\n
SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'\n
FLASKY_MAIL_SENDER = ''\n
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')\n\n
@staticmethod\n
def init_app(app):\n
pass\n\nclass DevelopmentConfig(Config):\n
DEBUG = True\n
MAIL_SERVER = ''\n
AMIL_PORT = 25\n
MAIL_USE_TLS = True\n
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')\n
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')\n
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')\n\nclass TestingConfig(Config):\n
TESTING = True\n
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')\n\nclass ProductionConfig(Config):\n
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///' + os.path.join(basedir, 'data.sqlite')\n\nconfig = {\n
'development': DevelopmentConfig,,\n
'testing': TestingConfig,\n
'production': ProductionConfig,\n\n
'default': DevelopmentConfig\n}\n程序为不同的环境选择了不同的数据库。7.3 程序包程序包即app文件夹,保存了程序的所有代码、模板、静态文件。7.3.1 使用程序工厂函数程序的工厂函数为修改程序实例的配置提供了可能。在单一文件开发中,程序配置是写好的,在运行时程序实例已经创建,无法再修改配置。而使用工厂函数,则使得可以为开发、测试、部署等不同需要使用不同的配置以运行程序。app/___init___.py 程序包的构造文件from flask import Flask, render_template\nfrom flask_bootstrap import Bootstrap\nfrom flask_mail import Mail\nfrom flask_moment import Moment\nfrom flask_sqlalchemy import SQLAlchemy\nfrom config import config\n\nbootstrap = Bootstrap()\nmail = Mail()\nmoment = Moment()\ndb = SQLAlchemy()\n\ndef create_app(config_name):\n
Flask(__name__)\n
app.config.from_object(config[config_name])\n
config[config_name].init_app(app)\n\n
bootstrap.init__app(app)\n
mail.init__app(app)\n
moment.init__app(app)\n
db.init__app(app)\n\n
return app\n7.3.2 使用蓝本(Blueprint)对于新手而言,蓝本这东西显得有些难以理解。蓝本主要用于定义路由。 在单脚本程序中,程序实例存在于全局作用域中,路由可以直接使用app.route修饰器定义,但现在程序在运行时才创建,就只有在调用了工厂函数中的create_app()后才能使用app.route修饰器,但这时再定义路由就太是了。同样的,由于错误页面使用app.errorhandler修饰器定义,因此错误页面的定义也会遇到问题。蓝图就可以解决这些问题。蓝本中的路由需要在蓝本注册到程序上后,路由才能真正成为程序的一部分以供执行。 本书中,在程序中创建了一个包main用于保存蓝本,可以理解为,这个蓝本的名字就是mainapp/main/___init___.py 创建蓝本from flask import Blueprint\n\nmain = Blueprint('main', __name__)\n\nfrom . import views, errors\n实例化一个Blueprint类对象可以创建蓝本,有两个必须指定的参数:蓝本的名字和蓝本所在的包或模块。通常第二个参数使用__name__变量即可。 程序的路由保存在app/main/views.py模块中,错误处理程序保存在app/main/errors.py中。需要导入这两个模块以与蓝本关联。但些处在末尾导入,是为了避免循环导入,因为在views.py和errors.py中还要导入蓝本mainapp/___init___.py 注册蓝本def create_app(config_name):\n
# 之前的内容\n
from .main import main as main_blueprint\n
app.register_blueprint(main_blueprint)\n\n
return app\napp/main/errors.py 蓝本中的错误处理程序from flask import render_template\nfrom . import main\n\n@main.app_errorhandler(404)\ndef page_not_found(e):\n
return render_template('404.htm'), 404\n\n@main.app_errorhandler(500)\ndef internal_server_error(e):\n
return render_template('500.html'), 500\napp/main/views.py 蓝本中定义的程序路由from datetime import datetime\nfrom flask import render_template, session, redirect, url_for\n\nfrom . import main\nfrom .forms import NameForm\nfrom .. import db\nfrom ..models import User\n\n@main.route('/', methods=['GET', 'POST'])\ndef index():\n
form = NameForm()\n
if form.validate_on_submit():\n
# 前面的内容\n
return redirect(url_for('.index'))\n
return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False), current_time=datetime.utcnow())\n注意蓝本中的url_for()的用法。在单脚本程序中,index()视图函数的URL可以使用url_for('index')获取。但在蓝本中,Flask会为蓝本中的全部端点加上一个命名空间,以方便在不同的蓝本中使用相同的端点名定义视图函数而不会产生冲突。命名空间就是蓝本的名字。因此在蓝本中,视图函数index()注册的端点名是main.index,其URL用url_for('main.index')获取。此外,在蓝本中使用url_for()函数可以省略蓝本名,即如以上代码中,直接用url_for('.index')代替。但这种写法仅限于命名空间是当前请求所在的蓝本。如果是跨蓝本的重定向,则必须使用带有命名空间的端点名7.3.3 其它程序文件书中有简单提到但没有给出代码的几个文件,也是需要读者自行修改添加的:app/models.py 数据库模型文件from . import db\n\n# 定义Role和User模型\n# 也就是定义这两个表\nclass Role(db.Model):\n
__tablename__ = 'roles'\n
id = db.Column(db.Integer, primary_key=True)\n
name = db.Column(db.String(64), unique=True)\n
users = db.relationship('User', backref='role', lazy='dynamic')\n\n
def __repr__(self):\n
return '&Role %r&' % self.name\n\nclass User(db.Model):\n
__tablename__ = 'users'\n
id = db.Column(db.Integer, primary_key = True)\n
username = db.Column(db.String(64), unique=True, index=True)\n
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))\n\n
def __repr__(self):\n
return '&User %r&' % self.username\napp/email.py 电子邮件支持函数文件from threading import Thread\nfrom flask import current_app, render_template\nfrom flask_mail import Message\nfrom . import mail\n\n# 异步发送电子邮件的函数\ndef send_async_email(app, msg):\n
with app.app_context():\n
mail.send(msg)\n\ndef send_email(to, subject, template, **kwargs):\n
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])\n
msg.body = render_template(template + '.txt', **kwargs)\n
msg.html = render_template(template + '.html', **kwargs)\n
thr = Thread(target=send_async_email, args=[app, msg])\n
thr.start()\n
return thr\napp/main/forms.py 表单对象文件from flask_wtf import Form\nfrom wtforms import StringField, SubmitField\nfrom wtforms.validators import Required \n\nclass NameForm(Form):\n
name = StringField('What is your name?', validators=[Required()])\n
submit = SubmitField('Submit')\n其它的模板文件,也要放进template文件夹中:template/\n├── 404.html\n├── 500.html\n├── base.html\n├── index.html\n├── mail\n│
├── new_user.html\n│
└── new_user.txt\n└── user.html更关于蓝本的使用信息,可以参考—— —— 7.4 启动脚本manage.py 启动脚本#!/usr/bin/env python\n\nimport os\nfrom app import create_app, db\nfrom app.models import User, Role\nfrom flask_script import Manager, Shell\nfrom flask_migrate improt Migrate, MigrateCommand\n\napp = create_app(os.getenv('FLASK_CONFIG') or 'default')\nmanager = Manager(app)\nmigrate = Migrate(app, db)\n\ndef make_shell_context():\n
return dict(app=app, db=db, User=User, Role=Role)\n\nmanager.add_command(\"shell\", Shell(make_context=make_shell_context))\nmanager.add_command(\"db\", MigrateCommand)\n\nif __name__ == '__main__':\n
manager.run()\n应该知道的是,整个程序,到这里的app = create_app()一行才创建程序,传入一个环境变量作为配置名或使用默认配置。7.5 需求文件程序中应当包含一个requirements.txt文件,用于记录所有依赖包及其精确的版本号,以使程序可以更好地平移到其它系统中。这个需求文件可以使用pip命令生成:(venv) $ pip freeze & requirements.txt\n在安装(使用)了新的包或升级包之后,最好要更新这个文件。这个文件在我运行这个程序时内容如下:alembic==0.8.8\nblinker==1.4\nclick==6.6\ndecorator==4.0.10\ndominate==2.2.1\nFlask==0.11.1\nFlask-Bootstrap==3.3.7.0\nFlask-Mail==0.9.1\nFlask-Migrate==2.0.0\nFlask-Moment==0.5.1\nFlask-Script==2.0.5\nFlask-SQLAlchemy==2.1\nFlask-WTF==0.12\nipython==5.1.0\nipython-genutils==0.1.0\nitsdangerous==0.24\nJinja2==2.8\nMako==1.0.4\nMarkupSafe==0.23\npexpect==4.2.1\npickleshare==0.7.4\nprompt-toolkit==1.0.7\nptyprocess==0.5.1\nPygments==2.1.3\npython-editor==1.0.1\nsimplegeneric==0.8.1\nsix==1.10.0\nSQLAlchemy==1.0.15\ntraitlets==4.3.0\nvisitor==0.1.3\nwcwidth==0.1.7\nWerkzeug==0.11.11\nWTForms==2.1\n当要在新的环境下安装这些包以产生完全相同的虚拟环境时,可以使用以下命令:(venv) $ pip install -r requirements.txt\n7.6 单元测试tests/test_basics.pyimport unittest\n\nfrom flask import current_app\nfrom app import create_app, db\n\nclass BasicsTestCase(unittest.TestCase):\n
def setUp(self):\n
self.app = create_app('testing')\n
self.app_context = self.app.app_context()\n
self.app_context.push()\n
db.create_all()\n\n
def tearDown(self):\n
db.session.remove()\n
db.drop_all()\n
self.app_context.pop()\n\n
def test_app_exists(self):\n
self.assertFalse(current_app is None)\n\n
def test_app_is_testing(self):\n
self.assertTrue(current_app.config['TESTING'])\n这个测试使用了标准库中的unittest。setUp()和tearDown()方法分别在各测试前后运行,名字以test_开头的函数都作为测试进行。setUp()方法创建一个测试环境,类似运行中的程序。然后创建程序、激活上下文、创建数据库,这些上下文和数据库都会在tearDown()方法中删除。第一个测试确保程序实例存在,第二个测试确保程序在测试配置中运行为了运行单元测试,还需要在manage.py脚本中添加一一个自定义命令:@mand\ndef test():\n
\"\"\"Run the unit tests.\"\"\"\n
import unittest\n
tests = unittest.TestLoader().discover('tests')\n
unitttest.TextTestRunner(verbosity=2).run(tests)\n运行单元测试:(venv) $ python manage.py test\n更多关于单元测试模块的使用,可参考7.7 创建数据库这里使用Flask-Migrate跟踪数据库迁移:(venv) $ python manage.py db upgrade\n","updated":"T08:50:20.000Z","canComment":false,"commentPermission":"anyone","commentCount":7,"likeCount":1,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T16:50:20+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":7,"likesCount":1},"":{"title":"chapter8 - 用户认证","author":"xujingwuwei","content":"本章所讲的都是关于用户认证相关的知识。与我们平时在一般的网站上注册用户类似,包含有用户注册、用户登录、邮件验证这些,而为了保护用户的密码,则需要用到的一个密码加密的功能。8.1 Flask的认证扩展本章中新出现的包有以下三个:Flask-Login
是一个第三方扩展,用于管理已登录用户的用户会话。需要使用pip install flask-login安装Werkzeug
Flask内置包,用于计算密码散列值并进行核对。起到了前面所说的密码加密的作用itsdangerous
Flask内置包,用于生成核对加密安全令牌。即生成一个令牌,使得注册用户可以通过接收到的邮件中的链接来确认他所注册的账号8.2 密码安全性所谓密码安全性,大多数情况下,意思即是不应该直接存储密码本身,而应该存储加密后的序列。而且所使用的加密算法最好是不可逆的,即只有相同的密码才能产生相同的序列,这样就可以保证密码的安全性。本章中使用Werkzeug包中的security模块实现这个功能。并且只需要使用到两个函数,分别用于用户注册用户验证:generate_password_hash(password, method=pbkdf2:sha1, salt_length=8) 从函数名可看出,这个函数是用于将输入的密码进行加密,输出一个字符串形式的密码散列值,这个值就可以保存在数据中。通常来说,第2、3这两个参数使用默认值即可check_password_hash(hash, password) 这个函数即将上一个函数所生成的(通常是保存在数据库中的)密码散列值和用户输入的密码做比较,返回True就表明密码正确。自然,它不会是简单地进行等于或不等于的比较,具体的方便可以查看这个函数的源代码,代码文件位置:/lib/python3.*/site-packages/werkzeug/security.py为了实现密码的加密和验证功能,需要修改程序中的数据库模型文件:app/models.py 在User模型中加入密码散列form werkzeug.security import generate_password_hash, check_password_hash\n\nclass User(db.Model):\n
# 之前的内容\n
password_hash = db.Column(db.String(128))\n\n
@property\n
def password(self):\n
raise AttributeError('password is not a readable attribute')\n\n
@password.setter\n
def password(self, password):\n
self.password_hash = generate_password_hash(password)\n\n
def verity_password(self, password):\n
return check_password_hash(self.password_hash, password)\npassword_hash = db.Column(db.String(128))这一行是用于在数据表中加入password_hash字段,这是很明确的。而对于后面加入的三个函数,个人觉得,
@property\n
def password(self):\n
raise AttributeError('password is not a readable attribute')\n这个函数是为了把password设置成只读的,禁止直接读取数据库中的密码的值,因为在生成密码散列值后,密码就无法还原了。而第二个函数的作用则很明显,使用用户输入的密码生成密码散列,然后赋值给前面定义的password_hash变量,也就是把生成的散列值存储到数据库中的同名字段中。第三个函数的作用为验证用户密码,即将输入的密码与数据库中保存的密码作比较在shell中测试密码散列功能(venv) $ python manage.py shell\n&&& u = User()\n&&& u.password = 'cat'\n以上已经设置了用户u的密码为为cat,而如果此时直接读取该用户的密码,将会报错,错误信息即以上加入的第一个函数的返回信息:&&& u.password\n...\n...\nAttributeError: passowrd is not a readable attribute\n继续前面的测试,这时候我们来测试密码加密和密码验证功能:&&& u.password_hash\n'pbkdf2:sha1:1000$2UvSsqYn$94ed29fddfafe3d06c3'\n&&& u.verify_password('cat')\nTrue\n&&& u.verify_password('dog')\nFalse\n&&& u2 = User()\n&&& u2.password = 'cat'\n&&& u2.password_hash\n'pbkdf2:sha1:1000$frl1p75V$d637584adfb5afc53a1e2a9acf7e1'\n注意在上面的测试中,虽然u和u2都使用了相同的密码,但是它们的的散列散却是不同的写进单元测试中为了方便这个测试功能以后还可以使用,书中建议把以上测试写到单元测试里面tests/test_user_model.py 密码散列化测试import unittest\nfrom app.models import User\n\nclass UserModelTestCase(unittest.TestCase):\n\n
def test_password_setter(self):\n
u = User(password = 'cat')\n
self.assertTrue(u.password_hash is not None)\n\n
def test_no_password_getter(self):\n
u = User(password = 'cat')\n
with self.assertRaise(AttributeError):\n
u.password\n\n
def test_password_verification(self):\n
u = User(password = 'cat')\n
self.assertTrue(u.verify_password('cat'))\n
self.assertTrue(u.verify_password('dog'))\n\n
def test_password_salts_are_random(self):\n
u = User(password='cat')\n
u2 = User(password='cat')\n
self.assertTrue(u.password_hash != u2.password_hash)\n以上四个函数,第1个为测试密码是否为空;第2个测试直接获取用户密码的情况;第3个用于验证用户密码,使用了正确的密码cat和错误的密码dog来测试;第4个用于说明相同的密码所产生的密码散列值会因用户的不同而不同。8.3 创建认证蓝本用户认证蓝本应当放在auth文件夹中,即app/auth。创建方法与前面创建main蓝本类似。app/auth/___init___.py 创建蓝本from flask import Blueprint\n\nauth = Blueprint('auth', __name__)\n\nfrom . import views\napp/auth/views.py 蓝本中的路由和视图函数from flask import render_template\nfrom . import auth\n\n@auth.route('/login')\ndef login():\n
return render_template('auth/login.html')\n注意在以上代码中,认证蓝本所使用的模板文件在auth目录中,所以此时应该在模板文件夹app/templates中创建这个auth文件夹。app/___init___.py 注册蓝本书中使用的是“附加蓝本”这个说明,其实看回第7章中的main蓝本的注册代码段,就可以发现,其实所谓附加蓝本和注册蓝本,都是同个东西def create_app(config_name):\n
# 之前的代码\n
from .auth import auth as auth_blueprint\n
app.register_blueprint(auth_blueprint, url_prefix='/auth')\n\n
return app\n以上的注册蓝本的代码中,使用到了url_prefix这个可选参数。如果使用了这个参数,那么注册后蓝本中定义的所有路由都会加上这个指定的前缀。8.4 使用Flask-Login认证用户8.4.1 准备用于登录的用户模型要使用Flask-Login扩展,程序的User模型必须实现几个方法:方法说明:is_authenticated() 如果用户已经登录,必须返回True,否则返回Falseis_active() 如果允许用户登录,必须返回True,否则返回False。如果要禁用账户,可以返回Falseis_anonymous() 对普通用户必须返回Falseget_id() 必须返回用户的唯一标识符,使用Unicode编码字符串这4个方法可以在模型中人为直接实现,不过Flask-Login提供了一个UserMixin类,其包含了这些方法的默认实现,可以满足大多数需求。因此,修改后的User模型如下:app/models.py 修改User模型,支持用户登录from flask_login import UserMixin\n\nclass User(UserMixin, db.Model):\n
__tablename__ = 'users'\n
id = db.Column(db.Integer, primary_key = True)\n
email = db.column(db.String(64), unique=True, index=True)\n
username = db.Column(db.String(64), unique=True, index=True)\n
password_hash = db.Column(db.String(128))\n
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))\n应注意到的是,这里添加了email字段,让用户使用电子邮件地址登录。 还要在程序的工厂函数中初始化Flask-Loginapp/___init___.py 初始化Flask-Loginfrom flask_login import LoginManager\n\nlogin_manager = LoginManager()\nlogin_manager.session_protection = 'strong'\nlogin_manager.login_view = 'auth.login'\n\ndef create_app(config_name):\n
# 之前的代码\n
login_manager.init_app(app)\n
# 后面的代码\nLoginManager对象的session_protection属性可以设为None、basic或strong以提供不同的安全级别。login_view属性设置登录页面的端点最后,Flask-Login要求程序实现一个回调函数,使用特定的标识符加载用户。其实就是要能够通过用户的某个属性来找到用户,并返回用户对象给所调用的程序。app/models.py 加载用户的回调函数from . import login_manager\n\n@login_manager.user_loader\ndef load_user(user_id):\n
return User.query.get(int(user_id))\n这个函数接收一个Unicode字符串形式的用户标识符,如果能找到用户,则返回用户对象,否则返回None8.4.2 保护路由所谓保护路由也就是确保特定的页面只允许通过认证(即登录后)的用户访问,这个限制常见于各种论坛。Flask-Login提供了一个login_required修饰器以实现这个功能。这个功能应该在认证路由中的视图函数里实现,这点是书中没有讲到的:app/auth/views.py 保护路由from flask_login import login_required\n\n@app.route('/secret')\n@login_required\ndef secret():\n
return 'Only authenticated users are allowed!'\n如果未认证的用户访问到这个路由,Flask-Login会拦截请求,并把用户发往登录页面——这个应该是由login_required修饰器实现的。8.4.3 添加登录表单登录表单中包含一个用于输入email地址的文本字段、一个密码字段、一个“记住我”复选框和提交按钮app/auth/forms.py 登录表单from flask_wtf import Form\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.validators import Required, Length, Email\n\nclass LoginForm(Form):\n
email = StringField('Email', validators=[Required(), Length(1, 64), Email()])\n
password = PasswordField('Password', validators=[Required()])\n
remember_me = BooleanField('Keep me logged in')\n
submit = SubmitField('Log In')\n登录页面的模板app/templates/auth/login.html应该是这样子的,书中没有明确给出:{% extends \"bootstrap/wtf.html\" as wtf %}\n{% extends base.html %}\n\n{% block title %}Flasky - Login{% encblock %}\n\n{% block page_content %}\n&div class=\"page-header\"&\n
&h1&Login&/h1&\n&div&\n&div class=\"col-md-4\"&\n
{{ wtf.quick_form() }}\n&div&\n{% endblock %}\n此时,我们应该在web页面中给出登录和退出的选项,这个选项是位于导航条中的,而导航条是在base.html模板中实现的,所以要修改base.html模板:app/templates/base.html 导航条中的Sign In和Sign Out:&ul class=\"nav navbar-nav navbar-right\"&\n
{% if current_user.is_authenticated %}\n
&li&&a href=\"{{ url_for('auth.logout') }}\"&Sign Out&/a&&/li&\n
{% else %}\n
&li&&a href=\"{{ url_for('auth.login') }}\"&Sign In&/a&&/li&\n
{% endif %}\n&/ul&\n判断条件中的current_user由Flask-Login定义,在视图函数和模板中自动可用,它的值是当前登录的用户应当注意的是,以上代码中的if语句里面,书中使用的是is_authenticated(),其实是不对的,在执行的时候会出错,这个应该是一个变量,而不是一个方法,可能是在后来的版本中改进的。在修改其它页面时遇到这个地方也应当如此修改8.4.4 登入用户视图函数login()的实现如下:app/auth/views.py 登录路由from flask import render_template, redirect, request, url_for, flash\nfrom flask_login import login_user\nfrom . import auth\nfrom ..models import User\nfrom .forms import LoginForm\n\n@auth.route('/login', methods=['GET', 'POST'])\ndef login():\n
form = LoginForm()\n
if form.validate_on_submit():\n
user = User.query.filter_by(email=form.email.data).first()\n
if user is not None and user.verify_password(form.password.data):\n
login_user(user, form.remember_me.data)\n
return redirect(request.args.get('next') or url_for('main.index'))\n
flash('Invalid username or password.')\n
return render_template('auth/login.html', form=form)\n以上函数中,在渲染表单后,用表单中填写的email来从数据库中加载用户,即进行用户核对,看有没有这个用户,如果有,则继续验证用户密码,如果都正确了,则调用Flask-Login中的login_user()函数,在用户会话中把用户标记为已登录。login_user()函数的参数是要登录的用户。同时,当用户输入了账号信息,并点击了提交按钮时,表单会根据填写的表单信息进行URL重定向。这种重定向也有两个可能,即如果账号信息错误,则会返回到登录页面,显示登录表单,Flask-Login会把原地址(即用户是从哪里来的)保存在查询字符串的next参数中,这个参数可以从request.args字典中获取;而如果账号信息正确,登录成功,那就返回首页,即url_for('main.index')语句的作用所在8.4.5 登出用户app/auth/views.py 退出登录from flask_login import logout_user, login_required\n\n@auth.route('/logout')\n@login_required\ndef logout():\n
logout_user()\n
flash('You have been logged out.')\n
return redirect(url_for('main.index'))\n函数调用了Flask-Login中的logout_user()函数,删除并重设用户会话8.4.6 测试登录app/templates/index.html 修改首页模板以显示登录信息:{% extends \"base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block page_content %}\n&div class=\"page-header\"&\n
{% if current_user.is_authenticated %}\n
{{ current_user.username }}\n
{% else %}\n
Stranger\n
{% endif %}\n
&/h1&\n&/div&\n{% endblock %}\n注意,在这里,已经把quick_form()给去掉了。所以在前面章节中写进去的{% import \"bootstrap/wtf.html\" as wtf %}和{{ wtf.quick_form(form) }}在此应该删掉。至此,还没有创建用户注册功能,所以可以在shell中注册新用户。在shell中注册用户时,如果不是从github上checkout 8c并且执行数据库迁移,那么需要先执行db.create_all()创建数据表:(venv) $ python manage.py shell\n&&& db.create_all()\n&&& u = User(email='', username='john', password='cat')\n&&& db.session.add(u)\n&&& mit()\n8.5 注册新用户8.5.1 添加用户注册表单注册新用户的链接放在了登录表单下面app/auth/forms.py 用户注册表单from flask_wtf import Form\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.validators import Required, Length, Email, Regexp, EqualTo\nfrom wtforms import Validator\nfrom ..models import User\n\nclass RegistrationForm(Form):\n
email = StringField('Email', validators=[Required(), Length(1, 64), Email()])\n
username = StringField('Username', validators=[Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 'Usernames must have only letters, numbers, dots or underscores')])\n
password = PasswordField('Password', validators=[Required(), EqualTo('password2', message='Passwords must match.')])\n
password2 = PasswordField('Confirm password', validators=[Required()])\n
submit = SubmitField('Register')\n\n
def validate_email(self, field):\n
if User.query.filter_by(email=field.data).first():\n
raise ValidationError('Email already registered.')\n\n
def validate_username(self, field):\n
if User.query.filter_by(username=field.data).first():\n
raise ValidationError('Username already in use.')\n这个表单中定义了两个自定义函数。在Flask中,如果表单类中定义了以validate_开头且后面跟着字段名的方法,这个方法就和常规的验证函数一起调用。也就是说,这两个函数会自动调用以执行验证操作(个人理解)app/templates/auth/register.html 表单的登录模板{% extends base.html %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Register{% endblock %}\n\n{% block page_content %}\n&div class=\"page-header\"&\n
&h1&Register&/h1&\n&/div&\n&div class=\"col-md-4\"&\n
{{ wtf.quick_form(form) }}\n&/div&\n{% endblock %}\n在登录页面添加一个指向注册页面的链接:app/templates/auth/login.html 链接到注册页面在渲染表单之后添加,如下&div class=\"col-md-4\"&\n
{{ wtf.quick_form(form) }}\n
New User?\n
&a href=\"{{ url_for('auth.register') }}\"&\n
Click here to register\n
&/p&\n&/div&\n8.5.2 注册新用户其实就是把用户在注册表单中填写的有效信息添加到数据库中,在路由中实现app/auth/views.py 用户注册路由@auth.route('/register', methods=['GET', 'POST'])\ndef register():\n
form = RegistrationForm()\n
if form.validate_on_submit():\n
user = User(email=form.email.data, username=form.username.data, password=form.password.data)\n
db.session.add(user)\n
flash('You can now login.')\n
return redirect(url_for('auth.login'))\n
return render_template('auth/register.html', form=form)\n这个代码里面使用到了db上下文,但是却并没有提到要先导入。我们应要在代码顶部先把db对象导入。db是位于app/___init___.py文件中的,所以要从上一层导入:from .. import db\n8.6 确认账户也就是常见的发送验证邮件,然后让用户在邮件中确认注册信息8.6.1 使用itsdangerous生成确认令牌为了安全性、唯一性起见,需要让用户通过代码生成的令牌来确认,而不是通过用户ID,因为用户ID容易被猜测伪造。而这个令牌可以使用itsdangerous包来生成:(venv) $ python manage.py shell\n&&& from manage import app\n&&& from itsdangerous import TimedJSONWebSignatureSerializer as Serializer\n&&& s = Serializer(app.cinfig['SECRET_KEY'], expires_in = 3600)\n&&& token = s.dumps({'confirm': 23})\n&&& token\nb'eyJleHAiOjE0NzU4MTA0NzUsImFsZyI6IkhTMjU2IiwiaWF0IjoxNDc1ODA2ODc1fQ.eyJjb25maXJtIjoyM30.O76-lSqaMiRaxPs6AtSy4vVhBInCFCLsxaBoEIPWk7g'\n&&& data = s.loads(token)\n&&& data\n{'confirm': 23}\nTimedJSONWebSignatureSerializer类生成具有过期时间的JSON Web签名, 第一个参数是一个密钥,第二个参数是

我要回帖

更多关于 flask response csv 的文章

 

随机推荐