目录
一、项目简介
任务清单管理系统采用
B
/
S
架构,基于
Linux
平台开发。采用轻量级的
Web
服务器
Nginx
, 其后端实现建议采用基于 Python
语言的
Flask
开源
Web
框架,进而增强扩展性。数据库采用关系型数据库
Mariadb
,前端的技术栈使用
Bootstrap
框架。该系统面向学生或者企业员工,提供任务添加、任务删除、任务完成标记, 任务搜索 ,可视化操作、数据实时展现等功能,目的在于轻松查看自己和他人的工作安排,合理规划手头任务。
二、项目功能
就像一般的
Todo List
应用一样, 实现了以下功能:
-
管理数据库连接
-
列出现有的待办事项
-
创建新的待办事项
-
检索单个待办事项
-
编辑待办事项或标记待办事项
-
删除待办事项
三、技术分析
1、为什么选择
Flask?
-
Flask
是一个使用
Python
编写的轻量级
Web
应用框架。其
WSGI
工具箱采用
Werkzeug
,模板引擎则使用 Jinja2
。
Flask
使用
BSD
授权。
-
Flask
也被称为
“microframework”
,因为它使用简单的核心,用
extension
增加其他功能。
-
Flask
没有默认使用的数据库、窗体验证工具。
-
因此
Flask
是一个使用
Python
编写的轻量级
Web
应用框架。轻巧易扩展,而且够主流,有问题不怕找不到人问,最适合这种轻应用了。
2、为什么选择
Mariadb
?
-
MariaDB
数据库管理系统是
MySQL
的
一个分支,主要由开源社区在维护,采用
GPL
授权许可MariaDB 的目的是完全兼容
MySQL
,包括
API
和命令行,使之能轻松成为
MySQL
的代替品。
-
MariaDB
虽然被视为
MySQL
数据库的替代品,但它在扩展功能、存储引擎以及一些新的功能改进方面都强过 MySQL
。而且从
MySQL
迁移到
MariaDB
也是非常简单的
.
3、为什么选择
Bootstrap?
-
Bootstrap
是一个用于快速开发
Web
应用程序和网站的前端框架。
Bootstrap
是基于
HTML
、CSS、
JavaScript
的。具有移动设备优先、浏览器支持良好、容易上手、响应式设计等。
四、Flask
开发大型项目结构
1、项目结构
多文件
Flask
程序的基本结构
,
如下图所示
:

•
requirements.txt
列出了所有依赖包
,
便于在其他电脑中重新生成相同的虚拟环境
;
•
config.py
存储配置
;
•
manage.py
用于启动程序以及其他的程序任务。
2、配置文件选项
程序经常需要设定多个配置。一般分为开发、测试和生产环境
,
它们使用不同的数据库
,
不会彼此影响。
# config.py
"""
存储配置;
"""
import os
# 获取当前项目的绝对路径;
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
"""
所有配置环境的基类, 包含通用配置
"""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'westos secret key' SQLALCHEMY_COMMIT_ON_TEARDOWN = True
SQLALCHEMY_TRACK_MODIFICATIONS = True FLASKY_MAIL_SUBJECT_PREFIX = '[西部开源]' FLASKY_MAIL_SENDER = '976131979@qq.com'
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
""" 开发环境的配置信息 """
# 启用了调试支持,服务器会在代码修改后自动重新载入,并在发生错误时提供一个相当有用的调试 器。
DEBUG = True
MAIL_SERVER = 'smtp.qq.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME') or '976131979'
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') or '密码'
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data- dev.sqlite') class TestingConfig(Config):
""" 测试环境的配置信息 """
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data- test.sqlite')
class ProductionConfig(Config):
""" 生产环境的配置信息 """
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
3、程序工厂函数
1、为什么需要程序工厂函数
?
-
在单个文件中开发程序很方便
,
但却有个很大的缺点
,
因为程序在全局作用域中创建
,
所以无法动态修改配置。
-
运行脚本时,
程序实例已经创建
,
再修改配置为时已晚。这一点对单元测试尤其重要
,
因为有时为了提高测试覆盖度,
必须在
不同的配置环境中
运行程序。
-
这个问题的解决方法是
延迟创建程序实例
,
把创建过程移到可显式调用的工厂函数中。
-
这种方法不仅可以给脚本留出配置程序的时间,
还能够创建多个程序实例。
2、如何实现程序工厂函数
?
创建扩展类时不向构造函数传入参数, 在之前创建的扩展对象上调用
init_app()
可以完成初始化过程。
不使用程序工厂函数
:
app = Flask(__name__)
bootstrap = Bootstrap(app)
mail = Mail(app)
使用程序工厂函数
:
bootstrap = Bootstrap()
mail = Mail()
def create_app():
app = Flask(__name__)
bootstrap.init_app(app)
mail.init_app(app)
return app
app/__init__.py
文件详细代码如下
:
"""
程序工厂函数, 延迟创建程序实例
"""
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_sqlalchemy import SQLAlchemy
from config import config
bootstrap = Bootstrap()
mail = Mail()
db = SQLAlchemy()
def create_app(config_name='development'):
""" 默认创建开发环境的app对象 """
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
db.init_app(app)
# 附加路由和自定义的错误页面
# .........后续还需完善, 补充视图和错误页面
return app
4、蓝图
:
组件化开发
1)什么是蓝图
?
-
Flask
蓝图提供了模块化管理程序路由的功能,使程序结构清晰、简单易懂。
-
假如说我们要为某所学校的每个人建立一份档案,一个很自然的优化方式就是这些档案如果能分类管理,就是说假如分为老师、学生、后勤人员等类别,那么后续查找和管理这些档案就方便清晰许多。
-
Flask
的蓝图就提供了类似
“
分类
”
的功能。
2)为什么使用蓝图
?
-
将不同的功能模块化
-
构建大型应用
-
优化项目结构
-
增强可读性
,
易于维护
3)应用蓝图三部曲
?
蓝图的创建
:
app/auth/__init__.py
# 'auth'是蓝图的名称
# __name__是蓝图所在路径
auth =Blueprint('auth',__name__)
from . import views
蓝图对象上注册路由
,
指定静态文件夹
,
注册模版过滤器
:
app/auth/views.py
@auth.route('/')
def auth_home():
return 'auth_home'
注册蓝图对象
app/__init__.py
# url_prefix: 指定访问该蓝图中定义的视图函数时需要添加的前缀, 没有指定则不加; app.register_blueprint(auth,url_prefix='/auth')
访问网址
http://127.0.0.1:5000/auth/
可以查看到
auth_home
的内容。
任务清单蓝图的应用
auth
蓝图

蓝图的创建
:
app/auth/__init__.py
from flask import Blueprint
# 实例化一个 Blueprint 类对象可以创建蓝本, 指定蓝本的名字和蓝本所在的包或模块
auth = Blueprint('auth', __name__)
# 把路由和错误处理程序与蓝本关联, 一定要写在最后, 防止循环导入依赖;
from . import views, errors
蓝图对象上注册路由
,
指定静态文件夹
,
注册模版过滤器
:
app/auth/views.py
from . import auth
@auth.route('/login')
def login():
return 'login'
@auth.route('/logout')
def logout():
return 'logout'
注册蓝图对象
app/__init__.py
def create_app(config_name='development'):
"""
默认创建开发环境的app对象
"""
# ...... # 附加路由和自定义的错误页面
from app.auth import auth as auth_bp
app.register_blueprint(auth_bp) # 注册蓝本
from app.todo import todo as todo_bp
app.register_blueprint(todo_bp, ) # 注册蓝本
return app
5、启动脚本
顶级文件夹中的
manage.py
文件用于启动程序。
"""
用于启动程序以及其他的程序任务。
"""
from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager, Shell
from app import create_app, db
app = create_app('default')
manager = Manager(app)
migrate = Migrate(app, db)
def make_shell_context():
return dict(app=app, db=db)
# 初始化 Flask-Script、Flask-Migrate 和为 Python shell 定义的上下文。 manager.add_command("shell", Shell(make_context=make_shell_context)) manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
基于
Unix
的操作系统中可以通过下面命令执行脚本
:
# python3 manage.py runserver --help 获取详细使用参数
python3 manage.py runserver
效果如下
:

依次访问网址, 如果访问可以显示相关信息, 则成功。
-
http://127.0.0.1:5000/login
-
http://127.0.0.1:5000/logout
-
http://127.0.0.1:5000/todo/add/
-
http://127.0.0.1:5000/todo/delete/
6、依赖包文件
程序中必须包含一个
requirements.txt
文件
,
用于记录所有依赖包及其精确的版本号。
pip freeze > requirements.txt
创建一个和当前环境相同的虚拟环境
,
并在其上运行以下命令
:
pip install -r requirements.txt
7、单元测试
1、什么是单元测试
?
单元测试也称之为
“
模块测试
”
,是对程序设计中的最小单元
——
函数进行测试的一种方法,所谓测试,就是验证我们自己编写的方法能不能够得到正确的结果,即用方法得到的结果与真实结果进行比对,这就称之为测试。
2、如何实现单元测试
?
python
中有特别多的单元测试框架和工具,
unittest
,
testtools
,
subunit
,
coverage
,testrepository ,
nose, mox
,
mock
,
fixtures
,
discover
等等,先不说如何写单元测试,光是怎么运行单元测试就有N
多种方法。
unittest
,作为标准
python
中的一个模块,是其它框架和工具的基
础。
3、unittest
核心概念及关系
TestCase
的实例就是一个测试用例。什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(
setUp
)
,执行测试代码
(
run
)
,以及测试后环境的还原
(
tearDown
)
。元测试( unit test
)
的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。TestSuite 是多个测试用例的集合,
TestSuite
也可以嵌套
TestSuite
。
TestLoader
是用来加载
TestCase
到
TestSuite
中的,其中有几个
loadTestsFrom__()
方法,就是从各个地方寻找 TestCase
,创建它们的实例,然后
add
到
TestSuite
中,再返回一个TestSuite 实例。TextTestRunner 是来执行测试用例的,其中的
run(test)
会执行
TestSuite/TestCase
中的run(result)方法。TextTestResult 保存测试的结果,包括运行了多少测试用例,成功了多少,失败了多少等信
息。

测试范例
# tests/test_number.py i
mport random
import unittest
"""
单独执行测试用例: python3 -m unittest test_number.py
"""
class TestSequenceFunctions(unittest.TestCase):
"""
setUp() 和 tearDown() 方法分别在各测试前后运行,并且名字以 test_ 开头的函数都作为测试 执行。
"""
def setUp(self):
self.seq = list(range(10))
def test_shuffle(self):
# make sure the shuffled sequence does not lose any elements
random.shuffle(self.seq)
self.seq.sort()
self.assertEqual(self.seq, list(range(10)))
# should raise an exception for an immutable sequence
self.assertRaises(TypeError, random.shuffle, (1, 2, 3))
def test_choice(self):
element = random.choice(self.seq)
self.assertTrue(element in self.seq)
def test_sample(self):
with self.assertRaises(ValueError):
random.sample(self.seq, 20)
for element in random.sample(self.seq, 5):
self.assertTrue(element in self.seq)
def tearDown(self):
del self.seq
单独执行测试用例
:
python3 -m unittest test_number.py
运行结果如下
:

4、任务清单单元测试的应用
第一个测试确保程序实例存在。第二个测试确保程序在测试配置中运行。若想把
tests
文件夹作为包使用,
需要添加
tests/__init__.py
文件
,
不过这个文件可以为空
,
因为
unittest
包会扫描所有模块并查找测试。
# tests/test_basics.py
import unittest
from flask import current_app
from app import create_app, db
class BasicsTestCase(unittest.TestCase):
"""
setUp() 和 tearDown() 方法分别在各测试前后运行,并且名字以 test_ 开头的函数都作为测试执 行。
"""
def setUp(self):
"""
在测试前创建一个测试环境。
1). 使用测试配置创建程序
2). 激活上下文, 确保能在测试中使用 current_app
3). 创建一个全新的数据库,以备不时之需。
:return:
"""
self.app = create_app('testing')
self.app_context = self.app.app_context()
# Binds the app context to the current context.
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
# Pops the app context
self.app_context.pop()
def test_app_exists(self):
"""
测试当前app是否存在?
"""
self.assertFalse(current_app is None)
def test_app_is_testing(self):
"""
测试当前app是否为测试环境?
"""
self.assertTrue(current_app.config['TESTING'])
为了运行单元测试
,
你可以在
manage.py
脚本中添加一个自定义命令。
# manage.py
# manager.command 修饰器让自定义命令变得简单。修饰函数名就是命令名,函数的文档字符串会显示在帮 助消息中。
@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
单元测试可使用下面的命令运行
:
python manage.py test
运行效果如下
:

5、项目需求文档
文件
README.md
,
建议包含项目介绍, 项目功能, 项目技术栈和项目最终演示效果。
项目与
Github
Github
上新建一个仓库
Repositories

填写相关
Repositories
仓库信息

出现如下页面信息, 提示如何提交项目到
Github
上
:

初始化项目为
Git
仓库,将项目文件添加到暂存区, 提交到本地仓库, 最终上传至远程仓库
Github
。
# 初始化项目为Git仓库
git init
Initialized empty Git repository in
/home/kiosk/Desktop/201905python/Todolist/.git/
# 将所有项目文件添加到暂存区
git add *
# 提交到本地仓库
git commit -m "Flask任务清单管理系统(一): 大型项目结构化管理"
# 添加一个新的远程仓库, 第一次需要, 后面的不需要添加.
git remote add origin https://github.com/lvah/TodoList.git
# 上传项目至远程仓库`
Github git push -u origin master
Git
常用命令流程图

Git
命令快速查找手册



6、用户认证
大多数程序都要进行用户跟踪。用户连接程序时会进行身份认证
,
通过这一过程
,
让程序知道自己的身
份。程序知道用户是谁后
,
就能提供有针对性的体验。
7、技术要点
Werkzeug
中的
security
模块能够很方便地实现密码散列值的计算。这一功能的实现只需要
两个函数
,
分别用在注册用户和验证用户阶段。
generate_password_hash(password, method= pbkdf2:sha1 , salt_length=8)
:
密码加密
的散列值。
check_password_hash(hash, password)
:
密码散列值和用户输入的密码是否匹配
.
@property
是经典类中的一种装饰器,新式类中具有三种
:

8、核心代码
# app/models.py
from app import db
from werkzeug.security import generate_password_hash, check_password_hash
class Role(db.Model):
"""
用户类型
"""
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
users = db.relationship('User', backref='role')
def __repr__(self):
return '<Role % r>' % self.name
class User(db.Model):
"""用户"""
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128)) # 加密的密码
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
# generate_password_hash(password, method= pbkdf2:sha1 , salt_length=8)
:密码加密的散列值。
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
# check_password_hash(hash, password) :密码散列值和用户输入的密码是否匹配.
return check_password_hash(self.password_hash, password)
def __repr__(self):
return '<User % r>' % self.username
9、测试代码
# tests/test_user_model.py
import unittest
from app.models import User
class UserModelTestCase(unittest.TestCase):
""" 用户数据库模型测试 """
def test_password_setter(self):
"""测试新建的用户密码是否为空?"""
u = User(password='cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
"""测试获取密码信息是否报错?"""
u = User(password='cat') with
self.assertRaises(AttributeError): password =u.password
def test_password_verification(self):
"""测试加密密码和明文密码是否验证正确?"""
u = User(password='cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
def test_password_salts_are_random(self):
"""测试每次密码加密的加密字符是否不一致?"""
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
测试用例运行效果如下
:

五、Flask-Login
优化数据库模型
1、技术要点
用户登录程序后
,
他们的认证状态要被记录下来
,
这样浏览不同的页面时才能记住这个状态。
Flask-Login是个非常有用的小型扩展,
专门用来管理用户认证系统中的认证状态
,
且不依赖特定的认证机制。
Flask-Login
提供了一个
UserMixin
类
,
包含常用方法的默认实现
,
且能满足大多数需求。

2、核心代码
Flask-Login
在程序的工厂函数中初始化
,
修改文件
app/__init__.py
:
session_protection
属性提供不同的安全等级防止用户会话遭篡改。可以设为
:
-
None
-
‘basic’
-
‘strong’ :
记录客户端
IP
地址和浏览器的用户代理信息
,
如果发现异动就登出用户。
# app/__init__.py
from flask_login import LoginManager
# .......此处省略前面重复的代码
login_manager = LoginManager()
# session_protection 属性提供不同的安全等级防止用户会话遭篡改。 login_manager.session_protection = 'strong'
# login_view 属性设置登录页面的端点。
login_manager.login_view = 'auth.login'
def create_app(config_name='development'):
# .......
# 用户认证新加扩展
login_manager.init_app(app)
# ........
return app
app/models.py
:
修改
User
模型
,
支持用户登录
,
同时
Flask-Login
要求程序实现一个回调函数
,
使用指定
的标识符加载用户。
# app/models.py
from flask_login import UserMixin
from . import login_manager
""" Flask-Login 提供了一个 UserMixin 类,包含常用方法的默认实现,且能满足大多数需求。
1). is_authenticated 用户是否已经登录?
2). is_active 是否允许用户登录?False代表用户禁用
3). is_anonymous 是否匿名用户?
4). get_id() 返回用户的唯一标识符
"""
class User(UserMixin, db.Model):
"""用户"""
# .............
# 电子邮件地址email,相对于用户名而言,用户更不容易忘记自己的电子邮件地址。
email = db.Column(db.String(64), unique=True, index=True)
# .............
# 加载用户的回调函数;如果能找到用户,返回用户对象;否则返回 None 。
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
3、数据库创建
Flask-Migrate
插件提供
Alembic
(
Database migration
数据迁移跟踪记录)提供的数据库升级和降级的功能。它所能实现的效果有如 Git
管理项目代码一般。
在新数据库中创建数据表。可使用如下
Bash
命令创建数据表或者升级到最新修订版本
# 初始化数据库, 创建migrations目录,存放了所有迁移脚本。只需要执行一次。
python3 manage.py db init
# 创建迁移脚本
python3 manage.py db migrate
# 更新数据库
python3 manage.py db upgrade
1)用户注册
如果新用户想成为程序的成员
,
必须在程序中注册
,
这样程序才能识别并登入用户。程序的登录页面中要
显示一个链接
,
把用户带到注册页面
,
让用户输入电子邮件地址、用户名和密码。
2)用户注册表单
注册页面使用的表单要求用户输入电子邮件地址、用户名和密码。
# app/auth/forms.py:用户注册表单
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo, ValidationError
from app.models import User
class RegistrationForm(FlaskForm):
email = StringField('电子邮箱', validators=[
DataRequired(), Length(1, 64), Email()])
username = StringField('用户名', validators=[
DataRequired(), Length(1, 64),
Regexp('^\w*$', message='用户名只能由字母数字或者下划线组成')])
password = PasswordField('密码', validators=[
DataRequired(), EqualTo('repassword', message='密码不一致')])
repassword = PasswordField('确认密码', validators=[DataRequired()])
submit = SubmitField('注册')
# 两个自定义的验证函数, 以validate_ 开头且跟着字段名的方法,这个方法和常规的验证函数一起调 用。
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
# 自定义的验证函数要想表示验证失败,可以抛出 ValidationError 异常,其参数就是错 误消息。
raise ValidationError('电子邮箱已经注册.')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('用户名已经占用.')
3)用户注册业务逻辑
处理用户注册的过程没有什么难以理解的地方。提交注册表单
,
通过验证后
,
系统就使用用户填写的信息
在数据库中添加一个新用户。处理这个任务的视图函数如下所示
:
# app/auth/views.py:用户注册路由
from flask import request, redirect, url_for, flash, render_template
from flask_login import login_user, login_required, logout_user
from app import db
from app.auth.forms import RegistrationForm
from app.models import User
from . import auth
# .........
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User() user.email = form.email.data
user.username = form.username.data
user.password = form.password.data
db.session.add(user) flash('注册成功, 请登录')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
# ...........
4)用户注册前端渲染
登录页面使用的模板保存在
templates/auth/register.html
文件中。这个模板只需使用
Flask
Bootstrap
提供的
wtf.quick_form()
宏渲染表单即可。
# templates/auth/login.html
{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block navbar %}
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">注销</a></li>
{% else %} <li><a href="{{ url_for('auth.login') }}">登录</a></li>
{% endif %}
<p style="color: red">{{ get_flashed_messages() }}</p>
</ul>
{% endblock %}
{% block content %}
<h1 style="color:green">用户注册</h1>
{{ wtf.quick_form(form) }}
{% endblock %}
5)用户注册页面简易效果展示

7)用户登录
用户登录表单
用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、提交按钮。
# app/auth/forms.py:用户登录表单
class LoginForm(FlaskForm):
"""用户登录表单"""
email = StringField('电子邮箱', validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField('密码', validators=[DataRequired()])
submit = SubmitField('登录')
2、用户登录业务逻辑
根据用户提交的
从数据库中加载用户, 判断用户存在
?
如果存在
,
调用用户对象的
verify_password()
方法
,
其参数是表单中填写的密码。判断用户密码是否正确?
如果密码正确
,
则调用
Flask-Login
中的
login_user()
函数实现用户登录。
login_user()
函数的参数是要登录的用户
,
以及可选的
“
记住我
”
布尔值
,“
记住我
”
也在表单中填写。
如果值为
False ,
那么关闭浏览器后用户会话就过期了
,
所以下次用户访问时要重新登录。
如果值为
True ,
那么会在用户浏览器中写入一个长期有效的
cookie,
使用这个
cookie
可以复现用户
会话。
# app/auth/views.py
# ...........
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
# 调用 Flask-Login 中的 login_user() 函数,在用户会话中把用户标记为已登录。
# login_user() 函数的参数是要登录的用户,以及可选的“记住我”布尔值,“记住我”也在 表单中填写。
login_user(user)
return redirect(request.args.get('next') or url_for('auth.login')) flash('无效的用户名和密码.')
return render_template('auth/login.html', form=form)
# ........
1)用户登录前端渲染
登录页面使用的模板保存在
templates/auth/login.html
文件中。这个模板只需使用
Flask
Bootstrap
提供的
wtf.quick_form()
宏渲染表单即可。
# templates/auth/login.html
{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block navbar %}
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('auth.logout') }}">注销</a></li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">登录</a></li>
{% endif %}
<p style="color: red">{{ get_flashed_messages() }}</p>
</ul>
{% endblock %}
{% block content %}
<h1 style="color:green">用户登录</h1>
{{ wtf.quick_form(form) }}
{% endblock %}
2)用户登录页面简易效果展示

3)用户注销
为了登出用户
,
这个视图函数调用
Flask-Login
中的
logout_user()
函数
,
删除并重设用户会话。随后会显
示一个
Flash
消息
,
确认这次操作
,
再重定向到首页
,
这样登出就完成了。
# app/auth/views.py
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('用户注销成功.')
return redirect(url_for('auth.login'))
4)Github
与用户认证
命令行提交项目代码到
Github
, 命令如下
:
git add *
git commit -m "基于Flask的任务清单管理系统(二): 用户认证基本功能实现"
git push origin master
效果如下
:

单元测试技术详解
5、web
单元测试的分类
-
测试对象较独立,无需依赖于
cookie
之类的上下文
-
依赖于上下文
-
web
前端的测试。
3、测试方式
第一种类型只需要使用
unittest
的常规测试即可 ,第二种类型,例如对于login_required
类型的
endpoint
,可使用
app.test_client()
返回测试客户端,同时附带上合适的数据。推荐使用
flflask-testing
插件。同时,由于这类依赖比较常见,所 以推荐将其独立成类。
第三种类型可使用
selenium
,但是编写
selenium
工作量比较大,且不够直观,且不够只管,建议 使用其他方式或者人工测试
代码组织推荐:
测试时,依赖于某些数据,除非测试数据的增删改,否则建议编写
数据导入函数
(后续写),可以减少工作量
1)前端页面优化
下面的步骤是根据登录界面进行修改, 注册页面修改也是同样的方式。
1).
导入前端页面需要的静态资源, 如下图所示
:

2).
资源文件夹中包含
login.html
和
register.html
两个文件
,
拷贝文件到
templates/auth
目录,
如下图所示
:

3).
查找文件中访问静态资源的
html
代码并修改静态资源
:

修改静态资源如下
:

访问到下图页面, 即可认为静态资源位置修改成功, 就可以接着做下面的其他操作了。

4).
修改文件
templates/auth/login.html
表单提交需要的信息, 如下图所示
:

5).
实现分类闪现,详细的参考消息闪现
flflash
的文档。
当我们
flflash
闪现信息时, 指定闪现信息的类型, 便于前端的分类展示 ,修改文件
app/auth/views.py
如下所示
:

Bootstrap提供的闪现组件警告框网址
。 警告框组件通过提供一些灵活的预定义消息,为常见的用户动
。 警告框组件通过提供一些灵活的预定义消息,为常见的用户动
作提供反馈消息。

而闪现信息的显示在很多场景都会使用, 我们把它独立成一个文件
templates/flash.html
, 代码如下:

登录的前端删除自带的闪现代码,
include
刚才编写的前端文件
templates/auth/login.html
即可,
如下图所示
:


6).
为了让前端的
CSS
样式生效, 还需要在表单的字段域中租用前端的属性信息, 如下图所示
:

通过上面的一番折腾, 好看的登录页面和注册页面就搞定了, 可以开始注册和登录了
.
需要测试的几个
要点
:
-
页面是否完整显示
?
-
登录注册功能是否可以测试通过
?
-
闪现信息是否分类六示
?
六、用户邮箱验证
如何确认注册用户提供的信息是否正确
?
常用方式是验证电子邮件地址。用户注册后
,
程序会立即发送一封确认邮件。新账户先被标记成待确认状态,
用户按照邮件中的说明操作后
,
才能证明自己可以被联系上。账户确认过程中
,
往往会要求用户点击一个包含确认令牌的特殊 URL
链接。

确认令牌的特殊
URL
链接该如何设计
?
链 接 形 式是
http://www.example.com/auth/confirm/<id>
, id
是用户的
id
。代表账户 id确认成功。
出现的问题
: id
可任意指定, 从而确认任意账户。
解决方法:
id
进行安全加密后得到令牌。
如何生成包含用户
id
的安全令牌
?
itsdangerous
模块中的
TimedJSONWebSignatureSerializer
类生成具有过期时间的
JSON Web 签名
(
JSON Web Signatures,JWS
)

如何将这种生成和检验令牌的功能可添加到
User
模型中。可以按照下面的步骤实现
:
/register/
–
> /auth/email/confirm
1、邮箱验证数据库模型
模型中新加入了一个列
confifirmed
用来保存账户的确认状态。
generate_confifirmation_token()
方法生成一个令牌
,
有效期默认为一小时。
confifirm()
方法检验令牌和检查令牌中
id
和已登录用户
id
是否匹配
?
如果检验通过
,
则把新添加的
confifirmed
属性设为
True
# app/models.py
class User(UserMixin, db.Model):
# .................
confirmed = db.Column(db.Boolean, default=False)
def generate_confirmation_token(self, expiration=3600):
"""生成一个令牌,有效期默认为一小时。"""
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id})
def confirm(self, token):
""" 检验令牌和检查令牌中id和已登录用户id是否匹配?如果检验通过,则把新添加的 confirmed 属 性设为 True
"""
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
db.session.commit()
return True
由于模型中新加入了一个列用来保存账户的确认状态
,
因此要生成并执行一个新数据库迁移。 执行
shell
命令如下
:
# 对数据库(db)进行迁移(migrate)。-m选项用来添加迁移备注信息。
python3 manage.py db migrate -m "添加账户的确认状态"
# 生成了迁移脚本后,使用upgrade子命令即可更新数据库.
python3 manage.py db upgrade
2、用户注册验证邮件的业务逻辑
当前的
/register
路由把新用户添加到数据库中后
,
会重定向到
/index
。在重定向之前
,
这个路由需要发送确认邮件。
3、电子邮件模板准备
认证蓝本使用的电子邮件模板保存在
templates/auth/confirm.html
文件中
,
参考微信的模板。可在资料包中下载 email.html
文件。 修改如下
:
url_for()
函数中的
_external=True
参数要求程序生成完整的
URL


4、电子邮件配置信息准备

5、发送电子邮件的业务逻辑
如果发送了多封测试邮件
,
页面停滞了几秒钟
,
在这个过程中浏览器就像无响应一样。为了避免处理请求
过程中不必要的延迟
,
我们可以把发送电子邮件的函数移到后台线程中, 实现多线程发送用户验证邮
件。


6、注册与确认验证的业务逻辑
注册与发送验证邮件的视图函数
当前的
/register
路由把新用户添加到数据库中后
,
会重定向到
/index
。在重定向之前
,
这个路由需要发送
确认邮件
,
确认邮件信息参考验证邮件模板。
此处如果注册成功且验证通过, 应该跳转到网站的首页, 请自行编写主页的业务逻辑
.


确认账户的视图函数如下面代码所示。
Flask-Login
提供的
login_required
修饰器会保护这个路由
,
因此
,
用户点击确认邮件中的链接后
,
要先登录,
然后才能执行这个视图函数。这个函数先检查已登录的用户是否已经确认过
,
如果确认过
,
则重定向到首页,
因为很
显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来的额外工作。

过滤未确认的账户
每个程序都可以决定用户确认账户之前可以做哪些操作。比如
,
允许未确认的用户登录
,
但只显示一个未
确认页面
unconfirmed.html
,
这个页面要求用户在获取权限之前先确认账户。这一步可使用
Flask
提供
的
before_request
钩子完成。
对蓝图来说
, before_request
钩子只能应用到属于蓝图的请求上。若想在蓝图中使用针对程序全局请求
的钩子
,
必须使用
before_app_request
修饰器。
同时满足以下
3
个条件时
,
before_app_request
处理程序会拦截请求。
用户已登录
(
current_user.is_authenticated
必须返回
True )
。
用户的账户还未确认。
请求的端点
(
使用
request.endpoint
获取
)
不在
auth
蓝图中。访问
auth
路由要获取权限
,
因为这
些路由的作用是让用户确认账户或执行其他账户管理操作。
如果请求满足以上
3
个条件
,
则会被重定向到
/auth/unconfirmed
路由
,
显示一个确认账户
相关信息的页面。


未确认页面的
html
可自行设计, 此处给出范例代码
:

显示给未确认用户的页面提供了一个链接
,
用于请求发送新的确认邮件
,
以防之前的邮件丢失。重新发送
确认邮件的视图函数代码如下
:

综上所述我们的用户邮箱验证全部完成。
拥有程序账户的用户有时可能需要修改账户信息。下面这些操作可使用本章介绍的技术添
加到验证蓝本中。
修改密码
重设密码
为避免用户忘记密码无法登入的情况
,
程序可以提供重设密码功能。安全起见
,
有必要
使用类似于确认账户时用到的令牌。用户请求重设密码后
,
程序会向用户注册时提供的
电子邮件地址发送一封包含重设令牌的邮件。用户点击邮件中的链接
,
令牌验证后
,
会
显示一个用于输入新密码的表单。
修改电子邮件地址
七、基于
Flask
的任务清单管理系统
(
三
):
用户资料
为了让用户的资料页面更吸引人,我们可以在其中添加一些关于用户的其他信息。扩充了
User
模型,
添加了几个新字段。

last_seen
字段创建时的初始值也是当前时间,但用户每次访问网站后,这个值都会被刷新。我们可以
在
User
模型中添加一个方法完成这个操作:

每次收到用户的请求时都要调用
ping()
方法。由于
auth
蓝本中的
before_app_request
处 理程序会
在每次请求前运行,所以能很轻松地实现这个需求,代码修改如下
:

1、视图函数
专门创建蓝图
user
用于用户信息的处理, 包括资料展示, 密码修改, 图像上传, 邮箱地址更新等等用户操作。
user
蓝图的实现步骤
:
蓝图的创建
:
app/auth/__init__.py

蓝图对象上注册路由
,
指定静态文件夹
,
注册模版过滤器
:
app/auth/views.py

注册蓝图对象
app/__init__.py

2、前端页面
从资料库中下载
user.html
文件
# templates/user/user.html
修改内容如下
:
CSS
样式和
JS
样式位置的指定

获取当前登录用户的信息


显示效果如下
:

3、用户资料编辑
编辑表单的定义

1)视图函数
在显示表单之前,这个视图函数为所有字段设定了初始值。是通过把初始值赋值
给
form.<field
-name>.data 完成的
提交表单后,表单字段的
data
属性中保存有更新后的值,因此可以将其赋值给用户对象中的各字段,然后再把用户对象添加到数据库会话中。


2)前端页面
为了让用户能轻易找到编辑页面,我们可以在资料页面中添加一个链接。
文件
templates/user
–
profile.html
的内容和
templates/user/users.html
基本一致, 只是
body
中的内容变化如下
:

3)项目流程的完善
为了让用户点击进入到用户信息查看与编辑页面, 添加对应的超链接如下
:


4)效果展示


八、任务清单管理
1、数据库模型
用户:
任务清单
:
任务分类
:

2、表单文件
-
添加任务的表单
-
编辑任务的表单
-
添加分类的表单
3、视图函数文件
from flask import flash, url_for, redirect, render_template, request,
current_app
from flask_login import login_required, current_user
from app import db
from app.models import Todo, Category
from app.todo.forms import AddTodoForm, EditTodoForm, AddCategoryForm
from . import todo
@todo.route('/add/', methods=['GET', 'POST'])
@login_required
def add():
form = AddTodoForm()
if form.validate_on_submit():
# 获取用户提交的内容
content = form.content.data
category_id = form.category.data
# 添加到数据库中
todo = Todo(content=content, category_id=category_id, user_id=current_user.id )
db.session.add(todo)
flash('添加任务成功', category='success')
return redirect(url_for('todo.add'))
print(Category.query.all())
return render_template('todo/add.html', form=form)
# 编辑任务
@todo.route('/edit/<int:id>/', methods=['GET', 'POST'])
def edit(id):
form = EditTodoForm()
# *****重要: 编辑时需要获取原先任务的信息, 并显示到表单里面;
todo = Todo.query.filter_by(id=id).first()
form.content.data = todo.content
form.category.data = todo.category_id
if form.validate_on_submit():
# 更新时获取表单数据一定要使用request.form方法获取,
# 而form.content.data并不能获取用户更新后提交的表单内容;
content = request.form.get('content')
category_id = request.form.get('category')
# 更新到数据库里面
todo.content = content
todo.category_id = category_id
db.session.add(todo)
flash('任务已更新', category='success')
return redirect(url_for('todo.list'))
return render_template('todo/edit.html', form=form)
# 删除任务: 根据任务id删除
@todo.route('/delete/<int:id>/')
@login_required
def delete(id):
todo = Todo.query.filter_by(id=id).first()
db.session.delete(todo)
flash("删除任务成功", category='success')
return redirect(url_for('todo.list'))
# 查看任务
@todo.route('/list/')
@login_required
def list(page=1):
# 任务显示需要分页,每个用户只能查看自己的任务
todoPageObj = Todo.query.filter_by(
user_id=current_user.id).paginate(
# 在config.py文件中有设置;
page, per_page=current_app.config['PER_PAGE'])
return render_template('todo/list.html', todoPageObj=todoPageObj)
# 修改任务状态为完成
@todo.route('/done/<int:id>/')
@login_required
def done(id):
todo = Todo.query.filter_by(id=id).first()
todo.status = True db.session.add(todo) flash('修改状态成功')
return redirect(url_for('list'))
# 修改任务状态为未完成
@todo.route('/undo/<int:id>')
@login_required
def undo(id):
todo = Todo.query.filter_by(id=id).first()
todo.status = False
db.session.add(todo) flash("修改状态成功")
return redirect(url_for('list'))
@todo.route('/category/add/', methods=['GET', 'POST'])
@login_required
def category_add():
form = AddCategoryForm()
if form.validate_on_submit():
# 获取用户提交的内容
content = form.content.data
# 添加到数据库中
category = Category(name=content,
user_id=current_user.id )
db.session.add(category)
flash('添加分类成功', category='success')
return redirect(url_for('todo.category_add'))
print(Category.query.all())
return render_template('todo/category_add.html', form=form)
# 查看任务
@todo.route('/category/list/')
@login_required
def category_list(page=1):
print(page)
# 任务显示需要分页,每个用户只能查看自己的任务
categoryPageObj = Category.query.filter_by(
user_id=current_user.id).paginate(
# 在config.py文件中有设置;
page, per_page=current_app.config['PER_PAGE'])
return render_template('todo/category_list.html',
categoryPageObj=categoryPageObj)
4、分页展示
视图函数
@app.route('/')
def index():
page = int(request.args.get('page',1))
paginate = Students.query.paginate(page,2)
stus = paginate.items
return render_template('index.html','stus'=stus,'paginate'=paginate)
前端页面
{% if paginate.has_prev %}
<a href="{{ url_for('user.list') }}?page={{ paginate.prev_num }}">上一页</a>
{% endif %}
{% for i in paginate.iter_pages() %}
<a href="{{ url_for('user.list') }}?page={{ i }}">
{% if not i %}
...
{% else %}
{{ i }}
{% endif %}
</a>
{% endfor %}
{% if paginate.has_next %}
<a href="{{ url_for('user.list') }}?page={{ paginate.next_num }}">下一页</a>
{% endif %}
版权声明:本文为daidadeguaiguai原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。