如需转载请注明出处。
win10 64位、Python 3.6.3、Notepad++、Chrome 67.0.3396.99(正式版本)(64 位)
注:作者编写时间2018-01-17,linux、python 3.5.2
以下内容均是加入自己的理解与增删,以记录学习过程。不限于翻译,部分不完全照搬作者Miguel Grinberg的博客,版权属于作者,感谢他提供免费学习的资料。
传送门 | |||
---|---|---|---|
00 开篇 | 01 Hello world | 02 模板 | 03 Web表单 |
04 数据库 | 05 用户登录 | 06 个人资料和头像 | 07 错误处理 |
08 关注 | 09 分页 | 10 支持QQ邮箱 | 11 美化页面 |
12 时间和日期 | 13 I18n和L10n 翻译成中文 zh-CN | 14 Ajax(百度翻译API | 15 更好的App结构(蓝图) |
16 全文搜索 | 17 部署到腾讯云Ubuntu | 18 部署到Heroku | 19 部署到Docker容器 |
20 JavaScript魔法 | 21 用户通知 | 22 后台工作(Redis) | 23 应用程序编程接口(API) |
本章将学习到:如何在Flask应用程序中进行错误处理(策略)。
这里将暂时停止为microblog添加新功能,而是讨论处理bug的策略,因为它们可能总是无处不在。为了帮助说明此主题,故意在第6章中的代码中遗留一个bug。等待着我们去发现它。
在Flask应用程序中发生error时会发生什么?找到问题的最佳方法是亲自体验,即重现它。启动应用程序,并确保已有两个注册用户。用其中一个用户身份登录后(在此以用户名+密码 susan cat为例),打开【Profile
】页面并点击【Edit you profile
】链接。在个人资料编辑页面,尝试将用户名更改为已注册的另一个用户的用户名(以 belen为例)并提交,这将带来一个可怕的“Internal Server Error
”页面:
在cmd里的打印(即运行应用程序的终端会话),将看到这个Error的堆栈跟踪(stack trace)。它在调试Error时很有用,因为它们会显示这个堆栈中的调用序列,一直到产生Error的行:
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [13/Aug/2018 14:26:04] "GET /login?next=%2Fedit_profile HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:26:08] "GET /login HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:26:17] "POST /login HTTP/1.1" 302 -
127.0.0.1 - - [13/Aug/2018 14:26:17] "GET /index HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:26:50] "GET /user/susan HTTP/1.1" 200 -
127.0.0.1 - - [13/Aug/2018 14:27:24] "GET /edit_profile HTTP/1.1" 200 -
[2018-08-13 14:28:45,549] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
context)
File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\default.py", line 509, in do_execute
cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username
...
...
堆栈跟踪表明了bug是什么。目前应用程序允许用户更改用户名,且并不会验证新用户名是否跟系统中已有的其他用户名发生冲突。这个Error来自SQLAlchemy,它视图将新用户名写入数据库,但数据库拒绝了它,因为username列
定义了unique=True
。
重要的是要注意,呈现给用户的错误页面没有提供有关错误的大量信息,这很好!我们绝对不希望用户知道崩溃是由于数据库错误,或我正在使用数据库,又或在我的数据库中某些表和字段名称引起。所有这些信息都应该保留在内部。
有一些事情远非理想。上述错误页面很难看,跟应用程序布局不匹配。终端上的日志不断刷新,导致重要的堆栈跟踪信息被淹没,而我们不得不不断回顾它,以免遗漏。当然,我们有一个bug需要修复(fix)。我们将解决所有这些问题,但首先,来讨论下Flask的调试模式。
但在开发应用程序时,可启用调试模式,这是Flask在浏览器上直接运行一个友好调试器的模式。要激活调试模式,得先停止应用程序,然后设置以下环境变量:
(venv) D:\microblog>set FLASK_DEBUG=1
在设置FLASK_DEBUG
后,重新启动服务器。终端上的打印(输出)将与之前看到的略有不同:
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 216-201-609
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
现在再次让应用程序崩溃,以便在浏览器中查看交互式调试器(the interactive debugger ):
调试器允许我们展开每个堆栈帧并查看相应的源代码。还可以在任何框架上打开一个Python提示符,执行任何有效的Python表达式,如检查变量的值。
决不在生产服务器上以调试模式运行一个Flask应用程序 是非常重要的。调试器允许用户远程执行服务器中的代码,因此对于想侵入你的应用程序或服务器的恶意用户来说,这可能是一个意外礼物。作为额外的安全措施,在浏览器中运行的调试器开始锁定,并在第一次使用时将询问PIN号(flask run命令运行后可看到打印中的PIN号)
由于我们处于调试模式的主题,应该提到使用调试模式启用的第二个重要功能,即 重新加载器 reloader。这是一个非常有用的开发功能,可在修改源文件时自动重新启动应用程序。如果在调试模式下运行flask run
命令,则可以在应用程序上运行,并且每次保存文件时,应用程序都将重新启动以获取新代码。
要声明自定义错误处理程序,得使用@errorhandler装饰器
。在此,将错误处理程序放在一个新的app/errors.py模板中:
app/errors.py:自定义错误处理程序
from flask import render_template
from app import app,db
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
错误处理函数 与视图函数的工作方式非常相似。对于这俩个错误,将返回各自模板的内容。注意,两个函数都在模板后面返回第二个值,即错误代码编号。对于到目前为止,创建的所有视图函数,都不需要添加第二个返回值,因为默认值为200(成功响应的状态代码)。在这种情况下,这些是错误页面,所以希望响应的状态代码能够反映出来。
在数据库错误之后,可以调用500错误的错误处理程序,实际上是上面的用户名重复情况。为确保任何失败的数据库会话不会干扰模板触发的任何数据库访问,我们发出一个会话回滚(session rollback)。这将会使得会话重置为干净状态。
以下是404错误的模板:
app/templates/404.html:找不到错误模板
{% extends "base.html" %}
{% block content %}
File Not Found
{% endblock %}
这是500错误的模板:
app/templates/500.html:内部服务器错误模板
{% extends "base.html" %}
{% block content %}
An unexpected error has occurred
The administrator has been notified. Sorry for the inconvenience!
{% endblock %}
两个模板都继承自base.html
,因此错误页面具有与应用程序的常规页面相同的外观。
要使用Flask注册这些错误处理程序,需要在创建应用程序实例后导入新的app/errors.py模板:
app/__init__.py:导入错误处理程序
#...
from app import routes,models,errors
如果在终端会话中设置FLASK_DEBUG=0
,然后再次触发重复的用户错误,将看到一个稍微友好的错误页面。
(venv) D:\microblog>set FLASK_DEBUG=0
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
[2018-08-13 17:39:21,022] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\base.py", line 1193, in _execute_context
context)
File "d:\microblog\venv\lib\site-packages\sqlalchemy\engine\default.py", line 509, in do_execute
cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username
对错误采取积极主动的态度非常重要。如果应用程序的生产版本发生错误,我想立马知道。因此,想到的第一个解决方案是将Flask设置为在发生错误后立即向我发送电子邮件,并在电子邮件正文中显示错误的堆栈跟踪。
第一步是将电子邮件服务器详细信息添加到配置文件 config.py中:
microblog/config.py:电子邮件配置
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you will never guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['[email protected]']
电子邮件的配置变量包括:服务器、端口、启用加密链接的布尔标志、可选的用户名和密码。
5个配置变量来自它们的环境变量对应物。如果未在环境中设置电子邮件服务器,那么将使用它作为需要禁用电子邮件错误的标志。电子邮件服务器端口也可以在环境变量中给出,但如果未设置,则使用标准端口25。默认情况下不使用电子邮件服务器凭据,但可以根据需要提供。ADMINS
配置变量是将收到错误报告的电子邮件地址的列表,以便自己的电子邮件地址应该在这个列表中。【稍后会将如何在环境中设置】
Flask使用Python的logging包来编写日志,这个包已经能够通过电子邮件发送日志。发送有关错误的电子邮件需要做的是将一个SMTPHandler实例添加到Flask记录器对象,即app.logger
:
app/__init__.py:通过电子邮件记录错误
import logging
from logging.handlers import SMTPHandler
# ...
if not app.debug:
if app.config['MAIL_SERVER']:
auth = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
secure = None
if app.config['MAIL_USE_TLS']:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='no-reply@' + app.config['MAIL_SERVER'],
toaddrs=app.config['ADMINS'], subject='Microblog Failure',
credentials=auth, secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
根据上述代码,在没有打开调试模式(app.debug=False
),且配置中存在邮件服务器(MAIL_SERVER)时,才会启用电子邮件日志记录器。
由于必须处理许多电子邮件服务器中存在的可选安全选项,因此设置电子邮件记录器有点单调乏味。实际上上述代码创建了一个SMTPHandler
实例,设置了它的级别,以便它只报告错误而不是警告,信息或调试消息,最后将它
从Flask添加到app.logger
对象中。
有两种方法可测试此功能。最简单的方法是使用Python的SMTP调试服务器。这是一个虚拟的电子邮件服务器,它接收电子邮件,但不是发送它们,而是将它们打印到控制台。要运行此服务器,先打开第二个终端会话,并运行如下命令:
(venv) D:\microblog>python -m smtpd -n -c DebuggingServer localhost:8025
保持调试SMTP服务器运行,并返回到第一个终端,在环境中设置 set MAIL_SERVER=localhost
和MAIL_PORT=8025
。确保FLASK_DEBUG
变量设置为0
,或根本不设置,因为应用程序不会在调试模式中发送电子邮件。运行应用程序,并在此触发SQLAlchemy错误,以查看运行模拟电子邮件服务器的终端会话如何显示具有完整堆栈跟踪错误的电子邮件。操作过程如下:第一个终端会话
C:\Users\Administrator>d:
D:\>cd D:\microblog\venv\Scripts
D:\microblog\venv\Scripts>activate
(venv) D:\microblog\venv\Scripts>cd D:\microblog
(venv) D:\microblog>set MAIL_SERVER=localhost
(venv) D:\microblog>set MAIL_PORT=8025
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
第二个终端会话
(venv) D:\microblog>python -m smtpd -n -c DebuggingServer localhost:8025
---------- MESSAGE FOLLOWS ----------
b'From: no-reply@localhost'
b'To: [email protected]'
b'Subject: Micblog Failure'
b'Date: Tue, 14 Aug 2018 09:47:40 +0800'
b'Content-Type: text/plain; charset="utf-8"'
b'Content-Transfer-Encoding: 7bit'
b'MIME-Version: 1.0'
b'X-Peer: ::1'
b''
b'Exception on /edit_profile [POST]'
b'Traceback (most recent call last):'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1193, in _execute_context'
b' context)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\default.py", line 509, in do_execute'
b' cursor.execute(statement, parameters)'
b'sqlite3.IntegrityError: UNIQUE constraint failed: user.username'
b''
b'The above exception was the direct cause of the following exception:'
b''
b'Traceback (most recent call last):'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 2292, in wsgi_app'
b' response = self.full_dispatch_request()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1815, in full_dispatch_request'
b' rv = self.handle_user_exception(e)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1718, in handle_user_exception'
b' reraise(exc_type, exc_value, tb)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask\\_compat.py", line 35, in reraise'
b' raise value'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1813, in full_dispatch_request'
b' rv = self.dispatch_request()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask\\app.py", line 1799, in dispatch_request'
b' return self.view_functions[rule.endpoint](**req.view_args)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\flask_login\\utils.py", line 261, in decorated_view'
b' return func(*args, **kwargs)'
b' File "D:\\microblog\\app\\routes.py", line 89, in edit_profile'
b' db.session.commit()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\scoping.py", line 153, in do'
b' return getattr(self.registry(), name)(*args, **kwargs)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 943, in commit'
b' self.transaction.commit()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 467, in commit'
b' self._prepare_impl()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 447, in _prepare_impl'
b' self.session.flush()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 2254, in flush'
b' self._flush(objects)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 2380, in _flush'
b' transaction.rollback(_capture_exception=True)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\langhelpers.py", line 66, in __exit__'
b' compat.reraise(exc_type, exc_value, exc_tb)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\compat.py", line 249, in reraise'
b' raise value'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\session.py", line 2344, in _flush'
b' flush_context.execute()'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\unitofwork.py", line 391, in execute'
b' rec.execute(self)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\unitofwork.py", line 556, in execute'
b' uow'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\persistence.py", line 177, in save_obj'
b' mapper, table, update)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\orm\\persistence.py", line 768, in _emit_update_statements'
b' execute(statement, multiparams)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 948, in execute'
b' return meth(self, multiparams, params)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\sql\\elements.py", line 269, in _execute_on_connection'
b' return connection._execute_clauseelement(self, multiparams, params)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1060, in _execute_clauseelement'
b' compiled_sql, distilled_params'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1200, in _execute_context'
b' context)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1413, in _handle_dbapi_exception'
b' exc_info'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\compat.py", line 265, in raise_from_cause'
b' reraise(type(exception), exception, tb=exc_tb, cause=cause)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\util\\compat.py", line 248, in reraise'
b' raise value.with_traceback(tb)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\base.py", line 1193, in _execute_context'
b' context)'
b' File "d:\\microblog\\venv\\lib\\site-packages\\sqlalchemy\\engine\\default.py", line 509, in do_execute'
b' cursor.execute(statement, parameters)'
b"sqlalchemy.exc.IntegrityError: (sqlite3.IntegrityError) UNIQUE constraint failed: user.username [SQL: 'UPDATE user SET username=?, about_me=? WHERE user.id = ?'] [parameters: ('belen', '', 1)] (Background on this error at: http://sqlalche.me/e/gkpj)"
------------ END MESSAGE ------------
这个功能的第二种测试方法是配置真实的电子邮件服务器。以下是使用Gmail账户的电子邮件服务器配置:
Gmail账户中的安全功能可能会阻止应用程序通过它发送电子邮件,除非我们明确允许“安全性较低的应用”访问您的Gmail帐户。在此处阅读此内容,如果担心帐户的安全性,可以创建仅为测试电子邮件而配置的辅助帐户,或者可以暂时启用安全性较低的应用程序来运行此测试然后还原到默认值。
https://myaccount.google.com/lesssecureapps
这表示Gmail开启了SMTP。
C:\Users\Administrator>d:
D:\>cd D:\microblog\venv\Scripts
D:\microblog\venv\Scripts>activate
(venv) D:\microblog\venv\Scripts>cd D:\microblog
(venv) D:\microblog>set MAIL_SERVER=smtp.googlemail.com
(venv) D:\microblog>set MAIL_PORT=587
(venv) D:\microblog>set MAIL_USE_TLS=1
(venv) D:\microblog>set MAIL_USERNAME=
(venv) D:\microblog>set MAIL_PASSWORD=
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
不知道是上述哪里设置错误。并未在本人的Gmail邮箱中收到邮件。改用国内QQ邮箱试试。步骤:
1)更改microblog/config.py
#...
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you will never guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
MAIL_USE_SSL = os.environ.get('MAIL_USE_SSL') is not None
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['[email protected]']
2)开启QQ邮箱 SMTP:打开邮箱、设置、账户、开启SMTP服务器、生成授权码。授权码赋值给MAIL_PASSWORD环境变量。
3)配置环境变量,并运行程序。1为True,0为False。
(venv) D:\microblog>set MAIL_SERVER=smtp.qq.com
(venv) D:\microblog>set MAIL_PORT=465
(venv) D:\microblog>set MAIL_USE_SSL=1
(venv) D:\microblog>set MAIL_USE_TLS=0
(venv) D:\microblog>set [email protected]
(venv) D:\microblog>set MAIL_PASSWORD=授权码
(venv) D:\microblog>flask run
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
然而并没有接收到邮件!!以后再测试了!
正确设置查看第10章 QQ邮箱 发送电子邮件部分
为了启用另一个基于文件类型RotatingFileHandler的日志记录器,需要以和电子邮件日志记录器类似的方式将其附加到应用程序的logger对象中。
app/__init__.py:电子邮件配置
# ...
from logging.handlers import RotatingFileHandler
import os
# ...
if not app.debug:
# ...
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Microblog startup')
在logs目录
中写入带有名称microblog.log
的日志文件,如果它尚不存在,那么将创建这个日志文件。
这个RotatingFileHandler类
很棒,因为它可切割、清理日志文件,以确保日志文件在应用程序运行很长一段时间也不会变得太大。在这种情况下,将日志文件大小限制为10kb,并且将最后10个日志文件保留为备份。
这个logging.Formatter类
提供自定义格式的日志消息。由于这些消息正在写入到一个文件,我们希望它们可存储尽可能多的信息。所以我们使用的格式包括 时间戳、日志记录级别、消息、日志来源的源代码文件、行号。
为使日志记录更有用,还将应用程序、日志记录级别降低到INFO
。如果不熟悉日志记录类别,它们分别是DEBUG、INFO、WARNING、ERROR、CRITICAL
(按严重程度递增)
日志文件第一个有趣的用途是 服务器每次启动时都会在日志中写入一行。当这个应用程序在生产服务器上运行时,这些日志数据将告诉我们服务器何时重新启动过。
应该还记得,RegistrationForm
已实现了对用户名的验证,但是编辑表单的要求略有不同。在注册过程中,我们需要确保在表单中输入的用户名不存在于数据库中。在编辑个人资料表单中,我们必须执行相同的检查,但有一个例外。如果用户保存原有用户名不变,则验证应该允许,因为该用户名已分配给该用户。下面将展示如何为这个表单实现用户名验证:
app/forms.py:在编辑个人资料表单中验证用户名
class EditProfileForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
submit = SubmitField('Submit')
#验证用户名
def __init__(self, original_username, *args, **kwargs):
super(EditProfileForm, self).__init__(*args, **kwargs)
self.original_username = original_username
def validate_username(self, username):
if username.data != self.original_username:
user = User.query.filter_by(username=self.username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
上述实现使用了一个自定义的验证方法,有一个重载的构造函数接受原始用户名作为参数。这个用户名保存为实例变量,并在validate_username()
方法中进行检查。如果在表单中输入的用户名与原始用户名相同,那么就没必要检查数据库是否有重复了。
要使用这个新验证方法,还需要在对应的视图函数中添加原始用户名到表单的username参数中:
app/routes.py:在编辑个人资料表单中验证用户名
#...
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm(current_user.username)
#...
现在修复了这个bug,并在大多数情况下,在编辑个人资料表单中出现用户名重复的提交将被友好地阻止。不过这不是一个完美的解决方案,因为当两个或多个进程同时访问数据库时,它可能不起作用。在这种情形下,竞争条件可能导致验证通过,但稍后当重命名时,数据库已经被另一个进程更改,并且无法重命名用户。除了非常繁忙的具有大量服务器进程的应用程序之外,这种情况不太可能发生,所以现在我们不用为此担心。
flask run
运行程序,再次重现错误,查看新表单验证方法如何阻止这个bug。
尝试更改不重名的username,效果:
查看数据库,看是否成功更改:
(venv) D:\microblog>sqlite3 app.db
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from user;
1|susan2018|[email protected]|pbkdf2:sha256:50000$OONOkVyy$8d008c6647ab95a5793cf60bf57eaa3bb1123d6e5b3135c5cc5e42e02eddae32|I rename my name.|2018-08-14 09:09:35.986028
2|belen|[email protected]|pbkdf2:sha256:50000$PEDt5NxS$cf6c958c97b6ad28d9495d138cb5a310f6f2389534b0cafa3002dd3cec9af9d1|学 习Flask超级教程,Python Web开发学习,坚持!|2018-08-13 03:54:02.884780
sqlite> .quit
目前为止,项目结构:
microblog/ app/ templates/ _post.html 404.html 500.html base.html edit_profile.html index.html login.html register.html user.html __init__.py errors.py forms.py models.py routes.py logs/ microblog.log migrations/ venv/ app.db config.py microblog.py
参考:
作者博客
如需转载请注明出处。