基于Flask框架的任务清单管理系统

目录

一、项目简介

二、项目功能

三、技术分析

1、为什么选择Flask?

2、为什么选择 Mariadb ?

3、为什么选择Bootstrap?

四、Flask开发大型项目结构

1、项目结构

2、配置文件选项

3、程序工厂函数

1、为什么需要程序工厂函数?

2、如何实现程序工厂函数?

4、蓝图: 组件化开发

5、启动脚本

6、依赖包文件

7、单元测试

1、什么是单元测试?

2、如何实现单元测试?

3、unittest 核心概念及关系

4、任务清单单元测试的应用

5、项目需求文档

6、用户认证

7、技术要点

8、核心代码

9、测试代码

五、Flask-Login优化数据库模型

1、技术要点

2、核心代码

3、数据库创建

2、用户登录业务逻辑

3、测试方式

 六、用户邮箱验证

1、邮箱验证数据库模型

2、用户注册验证邮件的业务逻辑

3、电子邮件模板准备

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

5、发送电子邮件的业务逻辑

6、注册与确认验证的业务逻辑

七、基于Flask的任务清单管理系统(三): 用户资料

1、视图函数

2、前端页面

3、用户资料编辑

八、任务清单管理

1、数据库模型

2、表单文件

3、视图函数文件

4、分页展示


一、项目简介

任务清单管理系统采用 B S 架构,基于 Linux 平台开发。采用轻量级的 Web 服务器 Nginx , 其后端实现建议采用基于 Python 语言的 Flask 开源 Web 框架,进而增强扩展性。数据库采用关系型数据库 Mariadb ,前端的技术栈使用 Bootstrap 框架。该系统面向学生或者企业员工,提供任务添加、任务删除、任务完成标记, 任务搜索 ,可视化操作、数据实时展现等功能,目的在于轻松查看自己和他人的工作安排,合理规划手头任务。

二、项目功能

就像一般的 Todo List 应用一样, 实现了以下功能:
  • 管理数据库连接
  • 列出现有的待办事项
  • 创建新的待办事项
  • 检索单个待办事项
  • 编辑待办事项或标记待办事项
  • 删除待办事项

三、技术分析

1、为什么选择Flask?

  1. Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 Flask使用 BSD 授权。
  2. Flask也被称为 “microframework” ,因为它使用简单的核心,用 extension 增加其他功能。
  3. Flask没有默认使用的数据库、窗体验证工具。
  4. 因此Flask是一个使用Python编写的轻量级Web应用框架。轻巧易扩展,而且够主流,有问题不怕找不到人问,最适合这种轻应用了。

2、为什么选择 Mariadb ?

  1. MariaDB 数据库管理系统是 MySQL一个分支,主要由开源社区在维护,采用 GPL 授权许可MariaDB 的目的是完全兼容 MySQL ,包括 API 和命令行,使之能轻松成为 MySQL 的代替品。
  2. MariaDB 虽然被视为 MySQL 数据库的替代品,但它在扩展功能、存储引擎以及一些新的功能改进方面都强过 MySQL 。而且从 MySQL 迁移到 MariaDB 也是非常简单的.

3、为什么选择Bootstrap?

  1. Bootstrap 是一个用于快速开发 Web 应用程序和网站的前端框架。Bootstrap 是基于 HTML、CSS、JavaScript 的。具有移动设备优先、浏览器支持良好、容易上手、响应式设计等。

四、Flask开发大型项目结构

1、项目结构

 
多文件 Flask 程序的基本结构 , 如下图所示 :
 
基于Flask框架的任务清单管理系统_第1张图片
 
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 = '[email protected]' 

@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、为什么需要程序工厂函数?

  1. 在单个文件中开发程序很方便,但却有个很大的缺点,因为程序在全局作用域中创建,所以无法动态修改配置。
  2. 运行脚本时,程序实例已经创建,再修改配置为时已晚。这一点对单元测试尤其重要,因为有时为了提高测试覆盖度,必须在不同的配置环境中运行程序。
  3. 这个问题的解决方法是延迟创建程序实例,把创建过程移到可显式调用的工厂函数中。
  4. 这种方法不仅可以给脚本留出配置程序的时间,还能够创建多个程序实例。

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)什么是蓝图 ?
  1. Flask蓝图提供了模块化管理程序路由的功能,使程序结构清晰、简单易懂。
  2. 假如说我们要为某所学校的每个人建立一份档案,一个很自然的优化方式就是这些档案如果能分类管理,就是说假如分为老师、学生、后勤人员等类别,那么后续查找和管理这些档案就方便清晰许多。
  3. 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 蓝图
 
  基于Flask框架的任务清单管理系统_第2张图片
 
 
蓝图的创建 : 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
效果如下 :
           基于Flask框架的任务清单管理系统_第3张图片
 
依次访问网址, 如果访问可以显示相关信息, 则成功。
  • 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 保存测试的结果,包括运行了多少测试用例,成功了多少,失败了多少等信
息。
 
                     基于Flask框架的任务清单管理系统_第4张图片
 
测试范例
# 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
运行结果如下 :
 
                      基于Flask框架的任务清单管理系统_第5张图片

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
运行效果如下 :
                          基于Flask框架的任务清单管理系统_第6张图片

5、项目需求文档

文件 README.md , 建议包含项目介绍, 项目功能, 项目技术栈和项目最终演示效果。
项目与 Github
Github 上新建一个仓库 Repositories
 
         基于Flask框架的任务清单管理系统_第7张图片
 
填写相关 Repositories 仓库信息
 
                     基于Flask框架的任务清单管理系统_第8张图片
出现如下页面信息, 提示如何提交项目到 Github :
 
基于Flask框架的任务清单管理系统_第9张图片
初始化项目为 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

                         基于Flask框架的任务清单管理系统_第10张图片

Git 常用命令流程图
                              基于Flask框架的任务清单管理系统_第11张图片
Git 命令快速查找手册
 
基于Flask框架的任务清单管理系统_第12张图片
 
基于Flask框架的任务清单管理系统_第13张图片
 
基于Flask框架的任务清单管理系统_第14张图片
 

6、用户认证

大多数程序都要进行用户跟踪。用户连接程序时会进行身份认证 , 通过这一过程 , 让程序知道自己的身
份。程序知道用户是谁后 , 就能提供有针对性的体验。

7、技术要点

Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要
两个函数 , 分别用在注册用户和验证用户阶段。
 
generate_password_hash(password, method= pbkdf2:sha1 , salt_length=8) : 密码加密
的散列值。
check_password_hash(hash, password) : 密码散列值和用户输入的密码是否匹配 .
@property 是经典类中的一种装饰器,新式类中具有三种 :
 
 
基于Flask框架的任务清单管理系统_第15张图片

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 '' % 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 '' % 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框架的任务清单管理系统_第16张图片
 

五、Flask-Login优化数据库模型

1、技术要点

用户登录程序后 , 他们的认证状态要被记录下来 , 这样浏览不同的页面时才能记住这个状态。 Flask-Login是个非常有用的小型扩展, 专门用来管理用户认证系统中的认证状态 , 且不依赖特定的认证机制。
Flask-Login 提供了一个 UserMixin , 包含常用方法的默认实现 , 且能满足大多数需求。
 
基于Flask框架的任务清单管理系统_第17张图片
 

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 %} 
     
{% endblock %} 
{% block content %} 
    

用户注册

{{ wtf.quick_form(form) }} {% endblock %}
5)用户注册页面简易效果展示
 
                      基于Flask框架的任务清单管理系统_第18张图片
7)用户登录
用户登录表单
用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、提交按钮。
# app/auth/forms.py:用户登录表单 

class LoginForm(FlaskForm): 

"""用户登录表单""" 
    email = StringField('电子邮箱', validators=[DataRequired(), Length(1, 64), 
Email()]) 
    password = PasswordField('密码', validators=[DataRequired()]) 
    submit = SubmitField('登录')

2、用户登录业务逻辑

根据用户提交的 email 从数据库中加载用户, 判断用户存在 ?
如果存在 , 调用用户对象的 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 %} 
     
        

{% endblock %} 
{% block content %} 
    

用户登录

{{ wtf.quick_form(form) }} {% endblock %}
2)用户登录页面简易效果展示
 
                       基于Flask框架的任务清单管理系统_第19张图片
 
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
效果如下 :
基于Flask框架的任务清单管理系统_第20张图片
单元测试技术详解
5、web 单元测试的分类
  • 测试对象较独立,无需依赖于cookie之类的上下文
  • 依赖于上下文
  • web前端的测试。

3、测试方式

第一种类型只需要使用 unittest 的常规测试即可 ,第二种类型,例如对于login_required 类型的 endpoint ,可使用 app.test_client() 返回测试客户端,同时附带上合适的数据。推荐使用flflask-testing 插件。同时,由于这类依赖比较常见,所 以推荐将其独立成类。
第三种类型可使用 selenium ,但是编写 selenium 工作量比较大,且不够直观,且不够只管,建议 使用其他方式或者人工测试
代码组织推荐:
测试时,依赖于某些数据,除非测试数据的增删改,否则建议编写 数据导入函数 (后续写),可以减少工作量
1)前端页面优化
下面的步骤是根据登录界面进行修改, 注册页面修改也是同样的方式。
1). 导入前端页面需要的静态资源, 如下图所示 :
 
                         基于Flask框架的任务清单管理系统_第21张图片
2). 资源文件夹中包含 login.html register.html 两个文件 , 拷贝文件到 templates/auth 目录,
如下图所示 :
 
                     基于Flask框架的任务清单管理系统_第22张图片
 
3). 查找文件中访问静态资源的 html 代码并修改静态资源 :
 
基于Flask框架的任务清单管理系统_第23张图片
 
修改静态资源如下 :
基于Flask框架的任务清单管理系统_第24张图片
访问到下图页面, 即可认为静态资源位置修改成功, 就可以接着做下面的其他操作了。
基于Flask框架的任务清单管理系统_第25张图片
4). 修改文件 templates/auth/login.html 表单提交需要的信息, 如下图所示 :
 
基于Flask框架的任务清单管理系统_第26张图片
5). 实现分类闪现,详细的参考消息闪现 flflash 的文档。
当我们 flflash 闪现信息时, 指定闪现信息的类型, 便于前端的分类展示 ,修改文件
app/auth/views.py 如下所示 :
基于Flask框架的任务清单管理系统_第27张图片
Bootstrap提供的闪现组件警告框网址 。 警告框组件通过提供一些灵活的预定义消息,为常见的用户动
作提供反馈消息。
 
 
基于Flask框架的任务清单管理系统_第28张图片
 
而闪现信息的显示在很多场景都会使用, 我们把它独立成一个文件 templates/flash.html , 代码如下:
基于Flask框架的任务清单管理系统_第29张图片
 
登录的前端删除自带的闪现代码, include 刚才编写的前端文件 templates/auth/login.html 即可,
如下图所示 :
基于Flask框架的任务清单管理系统_第30张图片
基于Flask框架的任务清单管理系统_第31张图片
 
6). 为了让前端的 CSS 样式生效, 还需要在表单的字段域中租用前端的属性信息, 如下图所示 :
 
 
基于Flask框架的任务清单管理系统_第32张图片
 
通过上面的一番折腾, 好看的登录页面和注册页面就搞定了, 可以开始注册和登录了 . 需要测试的几个
要点 :
  • 页面是否完整显示?
  • 登录注册功能是否可以测试通过?
  • 闪现信息是否分类六示?

 六、用户邮箱验证

如何确认注册用户提供的信息是否正确 ?
常用方式是验证电子邮件地址。用户注册后 , 程序会立即发送一封确认邮件。新账户先被标记成待确认状态, 用户按照邮件中的说明操作后 , 才能证明自己可以被联系上。账户确认过程中 , 往往会要求用户点击一个包含确认令牌的特殊 URL 链接。
 
基于Flask框架的任务清单管理系统_第33张图片
 
确认令牌的特殊 URL 链接该如何设计 ?
链 接 形 式是 http://www.example.com/auth/confirm/ , id 是用户的 id 。代表账户 id确认成功。
出现的问题 : id 可任意指定, 从而确认任意账户。
解决方法: id 进行安全加密后得到令牌。
如何生成包含用户 id 的安全令牌 ?
itsdangerous 模块中的 TimedJSONWebSignatureSerializer 类生成具有过期时间的 JSON Web 签名 ( JSON Web Signatures,JWS )
 
基于Flask框架的任务清单管理系统_第34张图片
 
如何将这种生成和检验令牌的功能可添加到 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
 
基于Flask框架的任务清单管理系统_第35张图片
基于Flask框架的任务清单管理系统_第36张图片
 

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

基于Flask框架的任务清单管理系统_第37张图片

5、发送电子邮件的业务逻辑

如果发送了多封测试邮件 , 页面停滞了几秒钟 , 在这个过程中浏览器就像无响应一样。为了避免处理请求
过程中不必要的延迟 , 我们可以把发送电子邮件的函数移到后台线程中, 实现多线程发送用户验证邮
件。
基于Flask框架的任务清单管理系统_第38张图片
 

6、注册与确认验证的业务逻辑

注册与发送验证邮件的视图函数
当前的 /register 路由把新用户添加到数据库中后 , 会重定向到 /index 。在重定向之前 , 这个路由需要发送
确认邮件 , 确认邮件信息参考验证邮件模板。
此处如果注册成功且验证通过, 应该跳转到网站的首页, 请自行编写主页的业务逻辑 .
基于Flask框架的任务清单管理系统_第39张图片
确认账户的视图函数如下面代码所示。
Flask-Login 提供的 login_required 修饰器会保护这个路由 , 因此 , 用户点击确认邮件中的链接后 , 要先登录, 然后才能执行这个视图函数。这个函数先检查已登录的用户是否已经确认过 , 如果确认过 , 则重定向到首页, 因为很
显然此时不用做什么操作。这样处理可以避免用户不小心多次点击确认令牌带来的额外工作。
 
基于Flask框架的任务清单管理系统_第40张图片
 
过滤未确认的账户
每个程序都可以决定用户确认账户之前可以做哪些操作。比如 , 允许未确认的用户登录 , 但只显示一个未
确认页面 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 路由 , 显示一个确认账户
相关信息的页面。
 
基于Flask框架的任务清单管理系统_第41张图片
基于Flask框架的任务清单管理系统_第42张图片
未确认页面的 html 可自行设计, 此处给出范例代码 :
 
基于Flask框架的任务清单管理系统_第43张图片
显示给未确认用户的页面提供了一个链接 , 用于请求发送新的确认邮件 , 以防之前的邮件丢失。重新发送
确认邮件的视图函数代码如下 :
 
基于Flask框架的任务清单管理系统_第44张图片
 
综上所述我们的用户邮箱验证全部完成。
拥有程序账户的用户有时可能需要修改账户信息。下面这些操作可使用本章介绍的技术添
加到验证蓝本中。
修改密码
重设密码
为避免用户忘记密码无法登入的情况 , 程序可以提供重设密码功能。安全起见 , 有必要
使用类似于确认账户时用到的令牌。用户请求重设密码后 , 程序会向用户注册时提供的
电子邮件地址发送一封包含重设令牌的邮件。用户点击邮件中的链接 , 令牌验证后 ,
显示一个用于输入新密码的表单。
修改电子邮件地址
 
 

七、基于Flask的任务清单管理系统(): 用户资料

为了让用户的资料页面更吸引人,我们可以在其中添加一些关于用户的其他信息。扩充了 User 模型,
添加了几个新字段。
基于Flask框架的任务清单管理系统_第45张图片
last_seen 字段创建时的初始值也是当前时间,但用户每次访问网站后,这个值都会被刷新。我们可以
User 模型中添加一个方法完成这个操作:
 
基于Flask框架的任务清单管理系统_第46张图片
 
每次收到用户的请求时都要调用 ping() 方法。由于 auth 蓝本中的 before_app_request 处 理程序会
在每次请求前运行,所以能很轻松地实现这个需求,代码修改如下 :
 
基于Flask框架的任务清单管理系统_第47张图片
 

1、视图函数

专门创建蓝图 user 用于用户信息的处理, 包括资料展示, 密码修改, 图像上传, 邮箱地址更新等等用户操作。
user 蓝图的实现步骤 :
蓝图的创建 : app/auth/__init__.py
 
基于Flask框架的任务清单管理系统_第48张图片
 
蓝图对象上注册路由 , 指定静态文件夹 , 注册模版过滤器 : app/auth/views.py
基于Flask框架的任务清单管理系统_第49张图片
 
注册蓝图对象 app/__init__.py
基于Flask框架的任务清单管理系统_第50张图片
 

2、前端页面

从资料库中下载 user.html 文件
# templates/user/user.html
修改内容如下 :
CSS 样式和 JS 样式位置的指定
基于Flask框架的任务清单管理系统_第51张图片
 
获取当前登录用户的信息
 
基于Flask框架的任务清单管理系统_第52张图片
 
基于Flask框架的任务清单管理系统_第53张图片
 
显示效果如下 :
基于Flask框架的任务清单管理系统_第54张图片
 

3、用户资料编辑

编辑表单的定义
基于Flask框架的任务清单管理系统_第55张图片
 
1)视图函数
在显示表单之前,这个视图函数为所有字段设定了初始值。是通过把初始值赋值 form. -name>.data 完成的
提交表单后,表单字段的 data 属性中保存有更新后的值,因此可以将其赋值给用户对象中的各字段,然后再把用户对象添加到数据库会话中。
 
基于Flask框架的任务清单管理系统_第56张图片
2)前端页面
为了让用户能轻易找到编辑页面,我们可以在资料页面中添加一个链接。
文件 templates/user - profile.html 的内容和 templates/user/users.html 基本一致, 只是 body
中的内容变化如下 :
基于Flask框架的任务清单管理系统_第57张图片
3)项目流程的完善
为了让用户点击进入到用户信息查看与编辑页面, 添加对应的超链接如下 :
基于Flask框架的任务清单管理系统_第58张图片
基于Flask框架的任务清单管理系统_第59张图片
 
4)效果展示
基于Flask框架的任务清单管理系统_第60张图片
基于Flask框架的任务清单管理系统_第61张图片
 

八、任务清单管理

1、数据库模型

用户:
任务清单 :
任务分类 :
 
基于Flask框架的任务清单管理系统_第62张图片
 

2、表单文件

  • 添加任务的表单
  • 编辑任务的表单
  • 添加分类的表单

基于Flask框架的任务清单管理系统_第63张图片

基于Flask框架的任务清单管理系统_第64张图片

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//', 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//') 
@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//') 
@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/') 
@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 %} 
    上一页 
{% endif %} 

{% for i in paginate.iter_pages() %} 
     
        {% if not i %} 
          ... 
        {% else %} 
            {{ i }} 
        {% endif %} 
     
{% endfor %} 

{% if paginate.has_next %} 
    下一页 
{% endif %}

 

 
 
 
 
 
 
 
 
 
 
 

你可能感兴趣的:(基于Flask框架的任务清单管理系统)