今天介绍的是《Flask Web开发》一书的第一个完整的应用,结合了之前的WTF表单,SQLAlchemy数据库,Bootstrap扩展以及Flask-Mail发送邮件的功能。但目前只是一个单脚本的应用,也就是说所有的功能实现都在一个py文件中,后面会根据书中所讲,使用多文件应用的结构进行改写。
文章目录
这个应用实现的功能非常简单:用户通过输入框输入名字和角色,系统读取输入,与数据库进行匹配,并返回对应的HTML页面,同时向管理员发送一封邮件。这个网页用bootstrap进行了渲染。
一、准备工作
此应用主要就两部分,hello.py文件和templates文件夹。前者是python单脚本程序,后者存储网页模板,通过python文件中render_template()
函数调用。这里重点介绍hello.py这个程序。
1. 先疯狂导入
# hello.py
''' 这个应用中os库主要用于控制环境变量 '''
import os
''' 控制异步发邮件 '''
from threading import Thread
''' 下面是flask基本模块,一般进行开发都要用到 '''
from flask import Flask, render_template, session, redirect, url_for
''' 我们自己用模板写前端页面一般比较麻烦,或者简陋,因此引入bootstrap简化和美化开发 '''
from flask_bootstrap import Bootstrap
''' wtf表单,以及表单字段,表单验证 '''
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
''' ORM数据库,是一类通过操作python对象来处理数据库语句的高级抽象层 '''
from flask_sqlalchemy import SQLAlchemy
''' 数据库迁移,可以方便的将每次创建或更改的数据模型更新到应用中 '''
from flask_migrate import Migrate
''' 发送邮件模块 '''
from flask_mail import Mail, Message
2. 必要配置
首先要通过app = Flask(__name__)
创建应用实例,一般命名为app。然后对app进行配置。
配置使用app.config
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
''' WTF表单需要用到secret key,值可以随便写 '''
app.config['SECRET_KEY'] = 'flask'
''' 数据库路径使用当前路径下创建的data.sqlite路径 '''
app.config['SQLALCHEMY_DATABASE_URI'] = \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
''' 数据库是否该跟踪修改,一般都是False '''
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
''' 邮件相关,包括服务器,端口,用户名和密码 '''
app.config['MAIL_SERVER'] = 'smtp.163.com' # 建议使用163邮箱,总之不要用QQ
app.config['MAIL_PORT'] = 25
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
''' 邮件主题前缀,发件人,以及管理员(也就是之后的收件人)'''
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[FLASKY]'
app.config['FLASKY_MAIL_SENDER'] = '你的一个邮箱'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMINI')
其中MAIL_USERNAME
MAIL_PASSWORD
FLASKY_ADMIN
需要事先在环境变量中设置好。如果不知道怎么设置,可以点击这里。
3. 初始化
配置完成之后,接下来初始化第一步导入的各种模块的应用实例
bootstrap = Bootstrap(app)
db = SQLAlchemy(app)
''' 数据库迁移要用到数据库和实例app,所以这里有两个参数 '''
migrate = Migrate(app, db)
mail = Mail(app)
二、主要功能
上述三个步骤完成之后,接下来就是最重要的部分了。
为了实现前面所说的功能,首先需要有一个网页显示的表单,所以需要编写表单模板;
其次由于需要连接数据库,还需要编写数据库模型;
最后,如果用户是第一次登入,我们希望能收到一封邮件,因此还需要编写邮件相关。
好了,大概就这么多,让我们开始吧!
1. 表单模板
class NameForm(FlaskForm):
name = StringField("what's your name?", validators=[DataRequired()])
role = StringField('Are you a teacher or student?', validators=[DataRequired()])
submit = SubmitField("submit")
好了,就这。WTF表单模板一般都是这种模式,通过定义类来定义表单模板,继承自FlaskForm,普通输入框就是StringField,提交是SubmitField。如果有密码,使用PasswordField。之后的validators用来验证,传入一个列表,这里验证函数意思是不能为空。
2. 数据库模型
class Role(db.Model):
__tablename__ = 'roles'
''' db.Column表示一行或一个字段 '''
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)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
数据库模型也是通过自定义类来实现,这里实现了两个表,roles和users。每一个表都有id和name,__repr__方法实现可以打印字符串,实例对象创建之后就会打印出此字符串。
比较重要的一点是两种表的关系。这是一张一对多的表,一个角色role对应多个用户user,我们希望能通过roles能快速查询到所对应的所有用户users,或者反过来知道一个用户,快速得到他的角色。
操作方法是,在“多”的一方,也就是users表创建一个外键ForeignKey,指向roles表,将两张表连接起来,然后在“一”的一方,通过db.relationship
实现相互之间的关系。这一行代码的意思是,roles表中通过users
属性可以访问到users表,users表反向访问roles表通过属性role
实现。
3. 发送邮件函数
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
sender = app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=(app, msg))
thr.start()
return thr
三、成功在望
1. templates模板
先看一下之前提到的模板文件。模板文件就是显示网页的html文件,只不过不是完全的html,而是有变量、控制结构,从而可以根据python文件动态的实现网页展示。
这些html文件都放在根目录的templates文件夹下,当前的程序中templates文件夹是以下的结构:
- templates
- index.html
- base.html
- mail
- new_user.html
index.html是继承自基模板base.html的,而base.html又是继承自bootstrap/base.html。
你可能会看到{{ 名称 }},{% 语句 %}的形式,这是templates中所有html文件的语法形式。除了html自身的语法,要想表示一个变量,使用一对花括号{{ }}括起来,要想表示循环等控制结构,使用{% %}括起来。
同时对于bootstrap,没有写出来的语句默认就是继承基模板的内容,要想对某一段内容进行自定义,要用{% block %} {% endblock %}这样的形式括起来。
- base.html
<!--templates/base.html-->
<!--
体会用法即可,目前不需要自己能写出来。只要知道这是在bootstrap模块下
重新编写的模板,用做当前这个应用的基模板
-->
{% extends "bootstrap/base.html" %}
<!-- extends 类似python中的import -->
{% block title %}Flasky{% endblock %}
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="/">Home</a></li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
index是上面视图函数中使用的模板,他是在基模板base.html的基础上改写的。
- index.html
<!-- templates/index.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<!--
这里的name,role, konwn是要通过python文件传进来的参数;
这些参数控制着整个html文件的内容
name&role: 如果用户刚访问页面,这两个值没有填,显示stranger,只要填写内容,就显示内容;
known: 如果填写的用户名在数据库中,known就是True, 否则就是False
-->
<h1>Hello, {% if name %}{{ role|capitalize }} {{ name }}{% else %}Stranger{% endif %}!</h1>
{% if not known %}
<p>Pleased to meet you!</p>
{% else %}
<p>Happy to see you again!</p>
{% endif %}
</div>
{{ wtf.quick_form(form) }}
<!-- 这里使用bootstrap的默认样式渲染传入的表单 -->
{% endblock %}
最后这个是发送邮件的邮件正文,非常简单。
- mail/new_user.html
User <b>{{ user.username }}</b> has joined.
2. 视图函数
最后一步是主要的python程序,用来将以上所有的小模块整合起来,实现最终功能。
# 定义一个路由,并给路由绑定一个函数,当输入路由地址,就调用绑定的这个函数
@app.route('/', methods=['POST','GET'])
def index():
'''实例化上面的表单模型'''
form = NameForm()
if form.validate_on_submit():
'''
WTF表单一行代码即可完成验证,由于我们的表单只有一个DataRequired,
因此只要有数据,这个判断就是True;
此时用户已经输入了数据,下一步要对输入数据与数据库进行匹配
'''
user = User.query.filter_by(username=form.name.data).first()
if user is None:
'''
如果数据库中查询不到用户名,说明是新用户;
然后看他输入的角色名是否已存在。由于之前数据库模型中Role对象的name字段设置了unique=True,
所以如果角色名已存在,不能再创建,而是直接用已存在的角色名;
如果角色名不存在,直接新建就可以了
'''
session['known'] = False
'''这个known变量就是后期要传入html模板的,至于为什么要用session,下面会讲的'''
role = Role.query.filter_by(name=form.role.data).first()
if role is None:
''' 角色名查询不到,直接新建 '''
role = Role(name=form.role.data)
db.session.add(role)
db.session.commit()
user = User(username=form.name.data, role_id=role.id)
db.session.add(user)
db.session.commit()
else:
''' 角色名查询到了,不用新建,只要新增用户名'''
user = User(username=form.name.data, role_id=role.id)
db.session.add(user)
db.session.commit()
'''发送邮件,调用上面的send_email函数'''
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
session['known'] = True
session['name'] = form.name.data
session['role'] = form.role.data
return redirect(url_for('index'))
'''
这里好像多此一举又return了一次,但是其实是有一定作用的;
使用reidrect重定向,使得提交表单之后的最后一个请求不是post而是get,
这样如果用户点击刷新,浏览器就不会弹出任何提示窗口,可以更丝滑;
但是变成get之后之前的name和known就没了,无法给模板传参了,怎么办?
我们可以将这些值存在用户会话session中,session可以像字典一样操作,
所以下面会看到session.get()的形式
'''
return render_template('index.html', form=form,
name=session.get('name'),
role=session.get('known'),
known=session.get('known', False))
四、测试运行
1. 运行
现在所有的代码均已写完,可以运行程序了!打开控制台,定位到当前文件夹,输入:
set FLASK_APP=hello.py
这样就把Flask中的环境变量和我们的python程序连接起来了。然后,输入:
set FLASK_DEBUG=1
开启调试模式,这样每次修改完代码,会自动重启服务,同时如果程序运行出错,会在浏览器直接显示错误页面,方便查看和调试
最后,运行Flask服务器:
flask run
这时你会发现,程序报错了,提示数据库不存在!我们之前只是建立了数据库模型,并没有把模型应用到程序中。这里需要用到准备工作中导入的Migrate。
首先,创建数据库迁移仓库,执行:
flask db init
然后,需要在这个迁移仓库中创建迁移脚本,将数据库模型导入
flask db migrate
最后,将数据库模型应用到数据库中
flask db upgrade
如果运行顺利了,根目录下会多出一个data.sqlite文件,这就是我们当前的数据库所在。再次运行:
flask run
程序就会正常运行了,访问http://127.0.0.1:5000/,就可以看到我们的这个应用程序了!试着输入一个姓名提交,看看你的邮箱中是否可以正常收到一封邮件吧!
2. 补全功能
在输入框输入数据,点击提交之后,这条数据是会写到数据库中的。如果多次操作之后,想要知道数据库中有哪些数据,需要调用控制台查看。首先ctrl+C
退出程序,输入python
进入python控制台。
>>>from hello import User,Role, db
>>>u = User.query.all()
[<User 'Susan'>, <User 'John'>, <User 'David'>]
>>> u[0].role
<Role 'student'>
这样就可以查看数据库了,但是每次都要先导入对象。现在还比较简单,后期如果数据库模型很多的话,会明显感觉到很麻烦。为了避免这一问题,可以使用flask shell命令自动导入。
app.shell_context_processor
装饰器可以创建一个shell上下文处理器,在hello.py中增加如下代码:
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
这样的话只要使用flask shell命令就可以自动将这些对象导入。
flask shell
>>>app
<Flask 'hello'>
>>>User
<class 'hello.User'>
>>>u = User.query.all()
[<User 'Susan'>, <User 'John'>, <User 'David'>]
3. python程序全代码
# hello.py
import os
from threading import Thread
from flask import Flask, render_template, session, redirect, url_for
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, RadioField
from wtforms.validators import DataRequired
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_mail import Mail, Message
basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'EZ'
app.config['SQLALCHEMY_DATABASE_URI'] =\
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['MAIL_SERVER'] = 'smtp.163.com'
app.config['MAIL_PORT'] = 25
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = '18217099317@163.com'
app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
bootstrap = Bootstrap(app)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
role = StringField('Are you a teacher or student?', validators=[DataRequired()])
submit = SubmitField('Submit')
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)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
role = Role.query.filter_by(name=form.role.data).first()
if role is None:
role = Role(name=form.role.data)
db.session.add(role)
db.session.commit()
# 血的教训,role要commit完再用role.id!
user = User(username=form.name.data, role_id=role.id)
db.session.add(user)
db.session.commit()
else:
user = User(username=form.name.data, role_id=role.id)
db.session.add(user)
db.session.commit()
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
role = Role.query.filter_by(name=form.role.data).first()
user.role_id = role.id
db.session.add(user)
db.session.commit()
session['known'] = True
session['name'] = form.name.data
session['role'] = form.role.data
return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'),
role=session.get('role'), known=session.get('known', False))
五、总结
这个应用虽然简单,但是已经涵盖了Flask Web开发基本的知识点,如果可以掌握的话对后期的学习是非常有帮助的。我学下来最难的就是bootstrap那块,因为要根据模板重新编写html页面,而如果想要使用bootstrap中的css和js,元素和各个属性的命名就要按照bootstrap中的命名规则来,不单独学习一下bootstrap的话是不行的。
正如前文所说,目前这个应用只有一个python脚本,全部的内容都在里面,看着凌乱不堪,非常难以维护。后期会改写程序,用多个文件来组织管理。