本项目将学习 Mariadb 作为数据库后端,Bootstrap 作为前端的技术栈,并实现一个清单应用。从中我们可以学习 Flask Web 应用框架,及 Mariadb 关系型数据库和 BootStrap web开发框架。
本应用修改自 TodoMVC 的 todo list 应用,使用 Mariadb 作为数据库后端,Bootstrap 作为前端的 Flask 应用。先给它起个好听的名字吧,方便之后称呼。
todo list => (自定义,随便起名称) => todoest
就像一般的 todo list 应用一样,todoest 实现了以下功能:
新建标签页,启动 todoest
打开浏览器访问 http://localhost:8000/
__ init __:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from flask_script import Manager
from flask_migrate import Migrate
from flask_moment import Moment
import pymysql
app = Flask(__name__)
# 数据库报错问题
pymysql.install_as_MySQLdb()
# 读取配置文件的配置信息
app.config.from_pyfile('../config.py')
db = SQLAlchemy(app)
manager = Manager(app)
bt = Bootstrap(app)
migrate = Migrate(app, db)
moment = Moment(app)
models(数据库表模板):
from app import db
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
# 用户和任务的关系: 一对多, 用户是一, 任务是多,
# 用户和分类的关系:一对多,用户是一,任务是多
# 用户表
class User(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
username = db.Column(db.String(20), unique=True)
password_hash = db.Column(db.String(100), nullable=False)
email = db.Column(db.String(30), unique=True)
add_time = db.Column(db.DateTime, default=datetime.now()) # 账户创建时间
# 1). User添加属性todos; 2). Todo添加属性user;
todos = db.relationship('Todo', backref="user")
categories = db.relationship('Category', backref='user')
@property
def password(self):
"""u.password"""
raise AttributeError("密码属性不可以读取")
@password.setter
def password(self, password):
"""u.password = xxxxx """
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
"""验证密码是否正确"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return "" %(self.username)
# 任务和分类的关系: 一对多
# 分类是一, 任务是多, 外键写在多的一端
# 任务表
class Todo(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
content = db.Column(db.String(100)) # 任务内容
status = db.Column(db.Boolean, default=False) # 任务的状态
add_time = db.Column(db.DateTime, default=datetime.now()) # 任务创建时间
# 任务的类型,关联另外一个表的id
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# 任务所属用户;
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return "" %(self.content[:6])
# 分类表
class Category(db.Model):
id = db.Column(db.Integer, autoincrement=True, primary_key=True)
name = db.Column(db.String(20), unique=True)
add_time = db.Column(db.DateTime, default=datetime.now()) # 任务创建时间
# 1). Category添加一个属性todos, 2). Todo添加属性category;
todos = db.relationship('Todo', backref='category')
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return "" %(self.name)
form(web表单):
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, \
ValidationError, SelectField, DateTimeField
from wtforms.validators import DataRequired, Email, Length, EqualTo
# 注册表单
from app.models import User, Category
class RegisterForm(FlaskForm):
email = StringField(
label="邮箱",
validators=[
DataRequired(),
Email(),
]
)
username = StringField(
label="用户名",
validators=[
DataRequired(),
],
)
password = PasswordField(
label='密码',
validators=[
DataRequired(),
Length(6, 12, "密码必须是6-12位")
]
)
repassword = PasswordField(
label='确认密码',
validators=[
EqualTo("password", "密码与确认密码不一致")
]
)
submit = SubmitField(
label="注册"
)
# *****************************************************
# 默认情况下validate_username会验证用户名是否正确, 验证的规则, 写在函数里面
def validate_username(self, field):
# filed.data ==== username表单提交的内容
u = User.query.filter_by(username=field.data).first()
if u:
raise ValidationError("用户名%s已经注册" % (u.username))
def validate_email(self, filed):
u = User.query.filter_by(email=filed.data).first()
if u:
raise ValidationError("邮箱%s已经注册" % (u.email))
# 登录表单
class LoginForm(FlaskForm):
username = StringField(
label="用户名",
validators=[
DataRequired(),
],
)
password = PasswordField(
label='密码',
validators=[
DataRequired(),
# Length(6, 12, "密码必须是6-12位")
]
)
submit = SubmitField(
label="登录"
)
# 关于任务的基类
class TodoForm(FlaskForm):
content = StringField(
label="任务内容",
validators=[
DataRequired()
]
)
# 任务类型
category = SelectField(
label="任务类型",
coerce=int,
choices=[(item.id, item.name) for item in Category.query.all()]
)
# 添加任务表单(继承任务的基类)
class AddTodoForm(TodoForm):
finish_time = DateTimeField(
label="任务终止日期"
)
submit = SubmitField(
label="添加任务",
)
# 编辑任务表单(继承任务的基类)
class EditTodoForm(TodoForm):
submit = SubmitField(
label="编辑任务",
)
views(视图函数):
import json
from functools import wraps
from app import app, db
from app.forms import RegisterForm, LoginForm, AddTodoForm, EditTodoForm
from flask import render_template, flash, redirect, url_for, session, request
from app.models import User, Todo
# 定义装饰器,判断用户是否登录
def is_login(f):
"""用来判断用户是否登录成功"""
@wraps(f)
def wrapper(*args, **kwargs):
# 判断session对象中是否有seesion['user'],
# 如果包含信息, 则登录成功, 可以访问主页;
# 如果不包含信息, 则未登录成功, 跳转到登录界面;;
if session.get('user', None):
return f(*args, **kwargs)
else:
flash("用户必须登录才能访问%s" % (f.__name__))
return redirect(url_for('login'))
return wrapper
# 网站首页
@app.route('/')
def index():
return redirect(url_for('list'))
# 注册页面
@app.route('/register/', methods=['POST', 'GET'])
def register():
form = RegisterForm()
if form.validate_on_submit():
# 1. 从前端获取用户输入的值;
email = form.email.data
username = form.username.data
password = form.password.data
# 2. 判断用户是否已经存在? 如果返回位None,说明可以注册;
u = User.query.filter_by(username=username).first()
if u:
flash("用户%s已经存在" % (u.username))
return redirect(url_for('register'))
else:
u = User(username=username, email=email)
u.password = password
db.session.add(u)
db.session.commit()
flash("注册用户%s成功" % (u.username))
return redirect(url_for('login'))
return render_template('register.html',
form=form)
# 登录页面
@app.route('/login/', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
username = form.username.data
password = form.password.data
# 1. 判断用户是否存在?
u = User.query.filter_by(username=username).first()
if u and u.verify_password(password):
session['user_id'] = u.id
session['user'] = u.username
flash("登录成功!")
return redirect(url_for('index'))
else:
flash("用户名或者密码错误!")
return redirect(url_for('login'))
return render_template('login.html',
form=form)
# 注销页面
@app.route('/logout/')
@is_login
def logout():
session.pop('user_id', None)
session.pop('user', None)
return redirect(url_for('login'))
# 添加任务
@app.route('/todo/add/', methods=['GET', 'POST'])
@is_login
def todo_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=session.get('user_id'))
db.session.add(todo)
db.session.commit()
flash("任务添加成功")
return redirect(url_for('todo_add'))
return render_template('todo/add_todo.html',
form=form)
# 编辑任务
@app.route('/todo/edit//', methods=['GET', 'POST'])
@is_login
def todo_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并不能获取用户更新后提交的表单内容;
# print(request.form)
# content = form.content.data # error
# category_id = form.category.data # error
content = request.form.get('content')
category_id = request.form.get('category')
# 更新到数据库里面
todo.content = content
todo.category_id = category_id
db.session.add(todo)
db.session.commit()
flash("更新任务成功")
return redirect(url_for('list'))
return render_template('todo/edit_todo.html',
form=form)
# 删除任务: 根据任务id删除
@app.route('/todo/delete//')
@is_login
def todo_delete(id):
todo = Todo.query.filter_by(id=id).first()
db.session.delete(todo)
db.session.commit()
flash("删除任务成功")
return redirect(url_for('list'))
# 查看任务
@app.route('/todo/list/')
@app.route('/todo/list//')
@is_login
def list(page=1):
# 任务显示需要分页
# Todo.query.paginate(page, per_page=5)
todoPageObj = Todo.query.filter_by(user_id=session.get('user_id')).paginate(page, per_page=app.config['PER_PAGE']) # 在config.py文件中有设置;
return render_template('todo/list_todo.html',
todoPageObj=todoPageObj,
)
# 修改任务状态为完成
@app.route('/todo/done//')
@is_login
def done(id):
todo = Todo.query.filter_by(id=id).first()
todo.status = True
db.session.add(todo)
db.session.commit()
flash("修改状态成功")
return redirect(url_for('list'))
# 修改任务状态为未完成
@app.route('/todo/undo/')
@is_login
def undo(id):
todo = Todo.query.filter_by(id=id).first()
todo.status = False
db.session.add(todo)
db.session.commit()
flash("修改状态成功")
return redirect(url_for('list'))
@app.route('/showTodo/')
@is_login
def showTodo():
done_count = Todo.query.filter_by(status=True).count()
undone_count = Todo.query.filter_by(status=False).count()
return render_template('todo/show_todo.html',
info={
'已完成': done_count,
'未完成': undone_count
})
# 通过echarts在前端显示任务完成与未完成的比例
@app.route('/newShowTodo/')
@is_login
def newShowTodo():
return render_template('todo/new_show_todo.html')
# 通过前端的ajax中的路由获取该视图函数返回的数据
# 因数据在前端的screpts标签中使用,
# 所以需要将python字典数据类型转换成js能识别的json格式
@app.route('/get_data/')
@is_login
def get_data():
done_count = Todo.query.filter_by(status=True).count()
undone_count = Todo.query.filter_by(status=False).count()
info = {
'info': ["已完成", "未完成"],
'count': [done_count, undone_count]
}
# ensure_ascii=False,解决中文编码问题
return json.dumps(info, ensure_ascii=False)
# 显示cup使用率
@app.route('/disk/')
def get_disk():
import psutil
cpuInfo = json.dumps({'CPU占有率', psutil.cpu_percent()},
ensure_ascii=False)
return cpuInfo
static(静态文件):
# css中的main.css:
.navbar {
font-size: 130%;
background: whitesmoke;
margin-top: 10px;
padding-top: 5px;
box-shadow: 2px 2px 2px 2px lightgray;
height: 60px;
}
# js中的echarts.min.js文件:
templates(html页面):
根据相应视图函数的功能编写适合的html页面,可通过bootstrap中文文档上的模板制作美观的页面。
config(配置文件):
# 数据库配置
SQLALCHEMY_DATABASE_URI = 'mysql://root:redhat@localhost/Todo'
SQLALCHEMY_TRACK_MODIFICATIONS = True
# 缓存安全配置,防止csrf(跨站请求伪造)
SECRET_KEY = 'westos'
# 每一页的数量
PER_PAGE = 5
manage(应用程序):
from app import manager, db
# 导入所有视图函数
from app.views import *
from flask_migrate import MigrateCommand
# 添加数据库操作的命令信息;
from app.models import User, Category, Todo
# 添加初始化数据库操作命令
@manager.command
def dbinit():
"""数据库初始化信息"""
db.drop_all()
db.create_all()
u = User(username='admin', email="[email protected]")
u.password = 'admin'
db.session.add(u)
db.session.commit()
print("用户%s创建成功......." % (u.username))
c = Category(name="学习", user_id=1)
db.session.add(c)
print("分类%s创建成功...." % (c.name))
t = Todo(content="学习Flask", category_id=1, user_id=1)
db.session.add(t)
print("任务%s添加成功....." % (t.content))
db.session.commit()
# 添加数据库迁移命令
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
# 在基模板引入moment模块
{% block script %}
{{ super()}}
{{ monent.include_moment()}}
{% endblock %}
@app.route('/')
def index():
return render_template('index.html',
current_time=datetime.utcnow())
The local date and time is {{ moment(current_time).format('LLL') }}.
That was {{ moment(current_time).fromNow(refresh=True) }}.
Flask 是python web开发的微框架,Echarts酷炫的功能主要是javascript起作用,将两者结合起来,发挥的作用更大。
# ajax中url路由对应的视图函数
# 前端获取后台数据
@app.route('/getdata')
def get_data():
language = ['python', 'java', 'c', 'c++', 'c#', 'php']
value = ['100', '150', '100', '90', '80', '90']
return json.dumps({'language':language,'value':value},ensure_ascii=False)
#如果有中文的话,就需要ensure_ascii=False
# 编写在scripts标签里面,$(function() {});是$(document).ready(function(){ })的简写
$(function () {
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
$.ajax({
url:'/getdata',
success:function (data) {
# 相当于python里面的将json格式解析为字典;
json_data=JSON.parse(data)
console.info(json_data['language'])
console.info(json_data['value'])
// 指定图表的配置项和数据
var option = {
title: {
text: '学习语言人数统计'
},
tooltip: {},
legend: {
data:['销量']
},
xAxis: {
data: json_data['language']
},
yAxis: {},
series: [{
name: '销量',
type: 'bar',
data: json_data['value']
}]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
}
})
})