一学就会-简单全面的Flask应用

今天介绍的是《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">&times;</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 }}&nbsp;{{ 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脚本,全部的内容都在里面,看着凌乱不堪,非常难以维护。后期会改写程序,用多个文件来组织管理。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值