三.Web表单
Flask-WTF对form进行封装,方便我们使用form表单
1.配置
Flask-WTF要求app设置一个密钥,防止CSRF跨域攻击
app.config["SECRET_KEY"] = "Titans"
//注:密钥最好不要写在源码里面
2.一个简单的实例
demo.py
from flask_wtf import FlaskForm
from wtforms import StringField,SubmitField
from wtforms.validators import DataRequired
from flask import Flask,render_template,request
class NameForm(FlaskForm):
name = StringField("what is your name",validators=[DataRequired()])
submit = SubmitField("Submit")
app = Flask(__name__)
app.config['SECRET_KEY'] = "Titans"
@app.route('/',methods=['GET','POST'])
def index():
name = None # 最开始访问index,name应该不存在
form = NameForm() # form的实例化
if request.method == 'POST':
if form.validate_on_submit():
name = form.name.data # 获取表单数据
form.name.data = '' # 重置表单数据
return render_template('my_form.html',name=name,form=form)
if __name__ == '__main__':
app.run()
my_form.html
{% extends "base.html" %}
{% block title %} My_form {% endblock %}
{% block body %}
Hello,{% if name %}{{ name }}
{% else %}stranger{% endif %}
{% endblock %}
总体思想:先创建一个表单类,继承自FlaskForm
然后编写属性,即对应表单的字段
之后在需要使用的时候进行表单实例化即可.
将实例对象传给模板,模板中就可以调用form的方法和属性生成表单.
然后当表单提交给后端时,后端检测到表单做出提交,做出响应即可.
同时如上可以给像html给每个字段指定id,class,attribute,name
3.表单类
在前面看到了一个表单类,这边详细说说如何编写它
class NameForm(FlaskForm):
name = StringField("what is your name",validators=[DataRequired()])
submit = SubmitField("Submit")
首先表单继承FlaskForm,然后编写对应字段,上面使用了StringField.
这边讲讲几种常用的字段:
BooleanField 复选框,值为True和False
FileField 文件上传字段
HiddenField 隐藏的文本字段
MultipleFileField 多文件上传
FloatField 浮点数
PasswordField 密码文本字段
RadioField 一组单选按钮
SelectField 下拉列表
SubmitField 表单提交按钮
StringField 文本字段
TextAreaField 多行文本字段
这边举一个图片上传的实例:
表单类加上file = FileField("上传文件")
当表单提交后执行:
file = request.files.get('file')
img_path = './static/' + "pic.png"
img = Image.open(file)
img.save(img_path)
前面看到DataRequired()
这边讲讲常用的validators
DataRequired 确保转换类型后字段中有数据
Email 验证电子邮件地址
EqualTo 比较两个字段的值,常用于要求输入两次密码进行确认的情况
Length 验证输入字符串长度
NumberRange 验证输入的值在数字范围之内
Optional 允许字段中没有输入,将跳过其它验证函数
flask-bootstrap和flask-wtf提供了一个快速渲染表单的函数,
使用前需要实例化Bootstrap.
{{ wtf.quick_form(form) }}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %} My_form {% endblock %}
{% block body %}
{{ wtf.quick_form(form) }}
{% endblock %}
4.在视图函数中处理表单
在上面完整实例中,index一共干了两件事情
一件是渲染表单,另一件是接受表单数据因此在路由设置methods
post处理表单数据,get处理渲染表单
在上诉表单中只有数据能被验证函数接受那么validate_on_submit()方法的返回值才为True
5.重定向和用户会话
前面在表单提交会出现一个问题,当表单提交后然后刷新页面,浏览器会警告再次提交页面。
该怎么解决这个问题呢?
重定向可以解决这个问题,重定向相当于重新发送了一个get请求到这个页面,这样就能解决这个问题.
但是重新发送请求的话,表单数据也会随之消失,因此我们需要保存一下提交表单的数据。
这时我们可以将数据存储在用户会话里面,一般情况下会话存储在客户端cookie中,采用前面app设置
的密钥加密前面,如果cookie发送变化,session也就随之消失了.session相当于保存于客户端的一个字典数据
@app.route('/',methods=['GET','POST'])
def index():
form = NameForm() # form的实例化
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for("index"))
# 等价于 return redirect("/")
return render_template('my_form.html',name=session.get('name'),form=form)
6.闪现消息(完整代码p42)
当请求结束后,应该让用户知道这个请求的状态
比如登录密码错误时,应该闪现消息来提示用户正确输入密码
后端调用直接flash("密码错误")
前端使用这个闪现消息
{% for message in get_flashed_message() %}
{{ message }}
{% endfor %}
这边使用了Boostrap渲染
四.数据库
1.SQL数据库:
主键:是每一行数据的唯一标识符
sql数据将数据以某种关系连接到一起,就像一张表一样呈现他们之间的关系
举个例子:
有一个roles的表,
存有id,name两个字段
另一个表users
存有id username password role_id字段
每张表中的每一行代表一个对象
同时两张表可以通过users.role_id和roles.id联系到一起,
这样就形成了一对多的关系.
常用:mysql,sqlite
2.NoSQL数据库:
NoSQL数据库常常以集合代替表,文档代替记录
常见:redis 以key-value存储数据(有点像session,但是它是数据库可以持久化保存数据)
一般情况下,NoSQL数据库的效率会高于SQL数据库
3.Flask-SQLAlchemy 管理数据库(ORM映射数据库)
简化了flask操作数据库,避免大量编写原生的SQL语句,同时也有效防止了SQL注入攻击.
(1)配置
app.config["SQLALCHEMY_DATABASE_URI"] ='mysql+pymysql://(username):(password)@(host):(port)/(database_name)'
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
(2)定义模型
定义模型类,需要继承自db.Model
class Role(db.Model):
__tablename__ = "roles"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64),unique=True)
def __repr__(self):
return f"
"
__tablename__是给表取的名字存放在数据库中的名称
常用的列类型 详情阅读p49
常用的列选项:
primary_key 设为True,表示为主键
unique 设为True,列不允许出现重复的值
index 设为True,为列创建索引,提升查询效率
nullable 设为True,表示允许为空
default 为列定义默认值
flask-sqlalchemy要求每个模型都定义主键,这一列通常取名为id
每个类,最好重写一个魔术方法__repr()__然后返回一个可读的字符串
4.关系
一对多:
class Role(db.Model):
users = db.relationship("User",backref="role")
class User(db.Model):
role_id = db.Column(db.Inter,db.ForeignKey("roles.id"))
通过user中的role_id外键建立起两个模型的关系,传给db.ForeignKey()的参数roles.id
表明这列的值是roles表中对应的Id
db.relationship()中的第一个参数表示关联User类,backref参数向User模型添加
一个role属性,从而反向关联,这样User的实例就可以直接访问role而不需要role_id
多对多的关系,只需要来一个中间表将一对多串起来.
5.数据库操作:
(1)创建表
db.creat_all()
(2)插入行
admin_role = Role(name="Admin")
user_role = Role(name="User")
mod_role = Role(name="Moderator")
user_john = User(username="john",role=user_role)
user_susan = User(username='susan',role=user_role)
user_david = User(username='david',role=user_role)
db.session.add_all([admin_role,mod_role,user_role,user_john,user_susan,user_david])
db.session.commit()
(3)修改行
admin_role = Role.query.filter(Role.name == "A").first()
admin_role.name = 'Admin'
db.session.add(admin_role)
db.session.commit()
(4)删除行
admin_role = Role.query.filter(Role.name == "Administrator").first()
db.session.delete(admin_role)
db.session.commit()
(5)查询行
全部查询:
roles = Role.query.all()
这时候我们前面重写的魔术方法就用上了,可以打印出我们想要的字符串
print(roles)
过滤查询:
user_role = Role.query.filter(Role.name == "User").first()
两种方式获取users
users = user_role.users
users = User.query.filter(User.role == user_role).all()
print(users)
在查询语句前加上str即可查看原生SQL语句
print(str(Role.query.filter(Role.name == "User")))
all()返回是一个查询结果列表,first()只返回一个结果,没有返回None
filter()过滤查询满足我们需求的数据
常用的查询过滤器
filter()
filter_by()
limit() 使用指定的值限制原查询返回的结果数量,返回一个新的查询
offset() 偏移原查询返回的结果,返回一个新的查询
order_by() 根据指定条件对原查询结果进行排序,返回一个新的查询
group_by() 根据指定条件对原查询结果进行分组,返回一个新的查询
常用查询执行方法
all() 以列表形式返回查询的结果
first() 返回查询的第一个结果,如果未查询到,返回None
count() 返回查询结果的数量
paginate() 返回一个paginate对象,包含指定范围内的结果
6.集成Python shell
@app.shell_context_processor
def make_shell_context():
return dict(db=db,User=User,Role=Role)
7.数据库迁移
from flask_migrate import Migrate
migrate = Migrate(app,db)
python manager.py db init 初始化迁移仓库(只要执行一次) # 只有第一次创建数据库才需要初始化
python manager.py db migrate -m "XXX注释"
python manager.py db upgrade 更新数据库
python manager.py db downgrade 还原前一个脚本对数据库的改动(可能会造成数据缺失)
五.电子邮箱
1.狗书中采用的是google邮箱,考虑到国外网络延迟,所有这边就演示使用QQ邮箱发送邮箱(网易163同理)
首先给app进行配置相应的信息,这边已经整理好了
app.config["MAIL_SERVER"] = "smtp.qq.com"
app.config["MAIL_PORT"] = 465 # 设置邮箱端口为465
app.config["MAIL_USE_SSL"] = True # QQ邮箱需要开启SSL
app.config["MAIL_USERNAME"] = "you@qq.com" # 填入你的qq号码
app.config["MAIL_PASSWORD"] = "code" # 填入qq邮箱的授权码(不懂可以自行百度搜索)
小提示:敏感信息最好不要写入源码中,从文件中读入,或者从环境导入
app配置好后
进行Mail实例化:
mail = Mail(app)
然后就可以在flask中发送邮箱了
@app.route('/email')
def email():
"""
Message发送邮件信息, sender为发送者邮箱, recipients为接受者邮箱
"""
message = Message("邮箱标题",sender=app.config["MAIL_USERNAME"],recipients=["接受邮箱@qq.com"])
message.body = "邮箱内容"
# 添加附件
# with app.open_resource("C:Usersfilename.doc") as doc:
# message.attach("my_file.doc",'application/octet-stream', doc.read())
send_email(message)
return "发送成功"
def send_email(msg):
with app.app_context():
mail.send(msg)
send函数调用了current_app所以需要app.app_context()激活应用上下文
实际开发时,可以将上面email路由抽象化,传递发送邮箱信息,接收者邮箱的参数,增加函数的灵活性.
2.此时已经可以发送邮箱,但是当发送邮箱数量大时,会发现速度有点慢。
这个时候我们会考虑到io堵塞,当上一封邮件未完全发完,而下一份邮件却在等它发送完毕,这样就造成效率的低下,
为了解决这个问题,采用异步发送邮箱
@app.route('/email')
def send_email():
email()
return "发送完成"
def email():
message = Message("邮箱标题",sender=app.config["MAIL_USERNAME"],recipients=["you@qq.com"])
message.body = "邮箱内容"
thr = Thread(target=send_async_email,args=[message])
thr.start()
return thr
def send_async_email(msg):
with app.app_context():
mail.send(msg)
六.大型应用的结构
1.项目结构
结构图详见p65
|-flasky
|-app/
|-templates/
|-static/
|-main/
|-__init__.py
|-errors.py
|-forms.py
|-views.py
|-__init__.py
|-email.py
|-models.py
|-migrations/
|-tests/
|-__init__.py
|-test*.py
|-venv/
|-requirements.txt
|-config.py
|-flasky.py
2.配置选项
应用程序通常需要多个配置环境,对应开发,测试,生成环境.
这样才不会相互造成影响
config.py
class Config:
SECRET_KEY = ""
MAIL_SERVER = ""
MAIL_PORT = ""
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = ""
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = ""
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = ""
config = {
"development" : DevelopmentConfig(),
"testing" : TestingConfig(),
"production" : ProductionConfig(),
"default" : DevelopmentConfig()
}
在父类Config包含共有的配置,然后在子类定义不同环境独有的配置,比如
不同环境中使用数据库不同,然后最后config用一个字典封装,方便使用时调用
,然后还注册一个默认的环境为开发环境
3.应用包
应用宝存放应用的所有代码、模板和静态文件。称为app,即上面的“|-app/”
templates和static目录为app的子目录。
同时数据库模型和电子邮件的支持函数也要移到这个包里面
app/models.py , app/email.py
(1)应用工厂函数
可以给脚本留出配置应用的时间,还能够创建多个应用实例,方便测试
app/__init__.py
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from config import config
bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
# config_name为需要选择配置环境的名称
def creat_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
# 注册蓝本
return app
(2)蓝本实现应用功能
与应用一样,蓝本可以在单个文件中定义,也可以使用更为结构化的方式在包中的多个模块中创建。
我们在应用包创建一个子包
app/main/__init__.py
from flask import Blueprint
main = Blueprint("main",__name__)
from . import views,errors
应用的路由一个保存在app/main/views.py
应用的错误处理保存在app/main/errors.py
但是如何将__init__ views errors关联起来呢?
只需在__init__ 的末尾加上from . import views,errors.
注意这边要是末尾,不然会导致循环导包
小提示from . import 是从当前包
from .. import 是从包的上一层
这时我们需要的是注册蓝本
app/__init__.py
def creat_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
# 注册蓝本
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
主蓝本中错误处理程序
app/main/errors.py
from flask import render_template
from . import main
@main.app_errorhandler(404)
def page_not_found(e):
pass
视图函数
app/main/views.py
from . import main
@main.route("/index")
def index():
pass
注意这边为main.route()
同时此时使用url_for应该使用main.index为端点名
4.应用脚本
顶层flasky.py
from app import creat_app,db
from app.models import User,Role
from flask_migrate import Migrate
app = creat_app("default")
migrate = Migrate(app,db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db,User=User,Role=Role)
使用
set FLASK_APP = flasky.py
set FLASK_DEBUG = 1
5.需求文件
requirements.txt说明flask使用依赖包,方便其它计算机运行该程序
pip freeze >requirements.txt 即可以自动生成
如果像创建这个虚拟环境的完整副本,需要先创建一个虚拟环境
然后运行pip install -r requirements.txt
6.单元测试
这边还没学过测试,就留着后面说了
7.创建数据库
flask db upgrade
8.运行
flask run