1.目标
本项目将学习 Mariadb 作为数据库后端,Bootstrap 作为前端的技术栈,并实现一个清单应用。从中我们可以学习 Flask Web 应用框架,及 Mariadb 关系型数据库和 BootStrap web开发框架。
2.项目介绍
本应用修改自TodoMVC的todo list应用,使用 Mariadb 作为数据库后端,Bootstrap 作为前端的 Flask 应用。
先给它起个好听的名字吧,方便之后称呼:
todo list => (自定义,随便起名称) => todoest
就像一般的 todo list 应用一样,todoest 实现了以下功能:
- 管理数据库连接
- 列出所有的 todo 项
- 创建新的 todo
- 检索单个 todo
- 编辑单个 todo 或将其标记为已完成
- 删除单个 todo
3.项目效果
新建标签页,启动 todoest
打开浏览器访问 http://localhost:8000/
4.技术分析
为什么选择Flask?
Flask是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。Flask使用 BSD 授权。
Flask也被称为 “microframework” ,因为它使用简单的核心,用 extension 增加其他功能。Flask没有默认使用的数据库、窗体验证工具。因此Flask是一个使用Python编写的轻量级Web应用框架。轻巧易扩展,而且够主流,有问题不怕找不到人问,最适合 todoest 这种轻应用了。
为什么选择Mariadb?
MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可 MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品。MariaDB虽然被视为MySQL数据库的替代品,但它在扩展功能、存储引擎以及一些新的功能改进方面都强过MySQL。而且从MySQL迁移到MariaDB也是非常简单的.
为什么选择Bootstrap?
Bootstrap是美国Twitter公司的设计师Mark Otto和Jacob Thornton合作基于HTML、CSS、JavaScript 开发的简洁、直观、强悍的前端开发框架,使得 Web 开发更加快捷。
Bootstrap中包含了丰富的Web组件,根据这些组件,可以快速的搭建一个漂亮、功能完备的网站。其中包括以下组件:下拉菜单、按钮组、按钮下拉菜单、导航、导航条、路径导航、分页、排版、缩略图、警告对话框、进度条、媒体对象等。
5.项目实现
创建项目
1.首先新创建一个 Flask 项目 TodoProject,并创建一个新的虚拟环境:
2.新建目录和文件,实现如下结构:
.
├── app
│ ├── forms.py
│ ├── init.py
│ ├── models.py
│ ├── static
│ ├── templates
│ └── views.py
├── config.py
└── manager.py
说明:
app:项目的容器。
config.py:该 Flask 项目的设置/配置
manage.py:一个实用的命令行工具,可让你以各种方式与该 Flask 项目进行交互。
app / forms.py:表单操作
app / init.py:一个空文件,告诉 Python 该目录是一个 Python 包。
app / models.py:数据库操作
app / static:存放 css 样式和 js 动效的目录
app / templates:存放所有的 .html 文件的目录
app / views.py:视图函数
2. 项目实现
1.创建一个数据库 Todo:
mysql -uroot -pwestos
create database Todo DEFAULT CHARSET utf8;
2.在 init.py 中导入需要用到类,并实例化其对象:
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
# 数据库报错问题
pymysql.install_as_MySQLdb()
app = Flask(__name__)
# 读取配置文件的配置信息
app.config.from_pyfile('../config.py')
db = SQLAlchemy(app)
manager = Manager(app)
bt = Bootstrap(app)
migrate = Migrate(app, db)
moment = Moment(app)
3.初始化数据库表:
先在 models.py 中定义数据库表(用户、任务和分类):
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)
# datetime.now()表示的只是你当前所在时区的时间;
# 使用协调时间时(Coordinated Universal Time,UTC)协调世界各地的时差问题;
# 美国时间: 2019-3-15 00:00 北京时间: 2019-03-16 12:00
add_time = db.Column(db.DateTime, default=datetime.utcnow()) # 账户创建时间
# 1). User添加属性todos; 2). Todo添加属性user;
todos = db.relationship('Todo', backref="user")
# 1). User添加属性categories; 2). Category添加属性user;
categories = db.relationship('Category', backref='user')
@property
def password(self):
"""读取密码:u.password"""
raise AttributeError("密码属性不可以读取")
@password.setter
def password(self, password):
"""加密密码:u.password = xxx"""
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
"""验证密码是否正确"""
return check_password_hash(self.password_hash, password)
def __repr__(self):
return "<User %s>" % (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.utcnow()) # 任务创建时间
# 任务的类型,关联分类表的id
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
# 任务所属用户,关联用户表的id
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return "<Todo %s>" % (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.utcnow())
# 1). Category添加一个属性todos; 2). Todo添加属性category
todos = db.relationship('Todo', backref='category')
# 分类所属用户,关联用户表的id
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return "<Category %s>" % (self.name)
在 manager.py 中创建数据库表并添加数据:
from app import manager
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="admin@qq.com")
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()
print('初始化数据库成功')
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
最后在命令行初始化数据库:
python manage.py db dbinit
运行结果:
初始化数据库成功
在数据库中查看数据库表
4.编辑视图函数 view.py 实现任务管理功能:
from app import app
# 网站首页
@app.route('/')
def index():
return 'index'
# 注册页面
@app.route('/register/')
def register():
return 'register'
# 登录页面
@app.route('/login/')
def login():
return 'login'
# 注销网页
@app.route('/logout/')
def logout():
return 'logout'
# 添加任务
@app.route('/todo/add/')
def todo_add():
return 'todo_add'
# 编辑任务
@app.route('/todo/edit/<int:id>/')
def todo_edit(id):
return "todo_edit %s" % (id)
# 删除任务
@app.route('/todo/delete/<int:id>/')
def todo_delete(id):
return "todo_delete"
# 查看任务
@app.route('/todo/list/')
@app.route('/todo/list/<int:page>')
def list(page):
return "list"
# 修改任务状态为完成
@app.route('/todo/done/<int:id>/')
def done(id):
return 'done'
# 修改任务状态为未完成
@app.route('/todo/undo/<int:id>')
def undo(id):
return 'undo'
5.在 form.py 文件中编辑需要的表单:
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="编辑任务",
)
6.在 templates 目录下创建 .html 文件实现页面:
基类(base.html)
{% extends 'bootstrap/base.html' %}
{% block styles %}
{# 先继承父类的css样式导入 #}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
{% endblock %}
{% block scripts %}
{{ super() }}
{# 实质是导入moment.js库的,通过Flask-moment集成了起来 #}
{{ moment.include_moment() }}
<script src="{{ url_for('static', filename='js/echarts.min.js') }}"></script>
{% endblock %}
{% block navbar %}
<nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<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="#">任务管理</a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">主页 <span class="sr-only">(current)</span></a></li>
<li><a href="{{ url_for('list') }}">用户信息</a></li>
<li><a href="#">监控</a></li>
<li><a href="#">新闻</a></li>
<li><a href="#">音乐</a></li>
<li><a href="{{ url_for('newShowTodo') }}">数据分析</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{# 分类讨论:
1. 如果没有登录,显示登录和注册按钮
2. 如果登录成功,显示用户名称和注销按钮
#}
{% if not session.user %}
<li><a href="{{ url_for('login') }}">登录</a></li>
<li><a href="{{ url_for('register') }}">注册</a></li>
{% else %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true"
aria-expanded="false">当前用户:{{ session.user }} <span class="caret"></span></a>
<ul class="dropdown-menu">
<li role="separator" class="divider"></li>
<li><a href="{{ url_for('logout') }}">注销</a></li>
</ul>
</li>
{% endif %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
{# 让每个页面都可以获取闪现信息闪现 #}
{% for item in get_flashed_messages() %}
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
aria-hidden="true">×</span></button>
{{ item }}
</div>
{% endfor %}
{% endblock %}
注册页面(register.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}注册页面{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-lg-offset-2">
<div class="page-header">
<h1>
注册
<small>
已有帐号
{# <a href="/login/">登录</a> #}
<a href="{{ url_for('login') }}">登录</a>
</small>
</h1>
</div>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
登录页面(login.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}登录页面{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-lg-offset-2">
<div class="page-header">
<h1>
登录
<small>
没有帐号
{# <a href="/login/">登录</a> #}
<a href="{{ url_for('register') }}">注册</a>
</small>
</h1>
</div>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
添加任务(todo / add_todo.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}添加任务{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-lg-offset-2">
<div class="page-header">
<h1>
添加任务
</h1>
</div>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
编辑任务(todo / edit_todo.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}添加任务{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-lg-offset-2">
<div class="page-header">
<h1>
添加任务
</h1>
</div>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
查看任务(todo / list_todo.html)
{% extends 'base.html' %}
{% block title %}任务显示{% endblock %}
{% block content %}
<div class="container">
<div class="col-lg-8 col-lg-offset-2">
<div class="page-header">
<h1>任务显示</h1>
</div>
<table class="table table-hover">
<tr>
<td>任务内容</td>
<td>任务状态</td>
<td>任务分类</td>
<td>所属用户</td>
<td>创建时间</td>
<td>操作</td>
</tr>
{% for todo in todoPageObj.items %}
<tr>
{% if todo.status %}
<td>
<del>{{ todo.content }}</del>
</td>
{% else %}
<td>{{ todo.content }}</td>
{% endif %}
<td>
{% if todo.status %}
<a href="{{ url_for('undo', id=todo.id) }}" style="color:green ">
<span class="glyphicon glyphicon-ok"
aria-hidden="true">
</span></a>
{% else %}
<a href="{{ url_for('done', id=todo.id) }}" style="color: mediumvioletred"><span
class="glyphicon glyphicon-remove"
aria-hidden="true">
</span></a>
{% endif %}
</td>
<td>{{ todo.category.name }}</td>
<td>{{ todo.user.username }}</td>
{# <td>{{ moment(todo.add_time).format('L') }}</td> #}
<td>{{ moment(todo.add_time).fromNow(refresh=True) }}</td>
<td>
<a class="btn btn-success" href="{{ url_for('todo_edit', id=todo.id) }}"
role="button">编辑</a>
<a class="btn btn-danger" href="{{ url_for('todo_delete', id=todo.id) }}"
role="button">删除</a>
</td>
</tr>
{% endfor %}
</table>
<nav aria-label="Page navigation">
<ul class="pagination">
{#
1. 上一页的显示url获取:
/list/2/ ===== url_for('list', todoPageObj.prev_num)
2. 上一页信息逻辑判断:
1). 判断是否有上一页信息
2). 如果有,创建链接
3). 如果没有,该链接设为不可点击的链接
3. 上一页显示使用的类属性和方法:
1). dataObj.has_prev:判断用户是否有上一页 ----> 如果有,返回True; 如果没有,返回False;
2).dataObj.prev_num:获取上一页的页数编号;
#}
{% if todoPageObj.has_prev %}
<li>
<a href="{{ url_for('list', page=todoPageObj.prev_num) }}" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
{% endif %}
{#
详细页的显示:
依次创建每个分页表框:
1). 是否为none,设置类名为diabled
2). 是否为当前页,设置类名为active
3).其他,正常设置
#}
{% for page in todoPageObj.iter_pages() %}
{% if page is none %}
<li class="disabled"><a href="#">......</a></li>
{% elif page == todoPageObj.page %}
<li class="active"><a href="{{ url_for('list', page=page) }}">{{ page }}</a></li>
{% else %}
<li><a href="{{ url_for('list', page=page) }}">{{ page }}</a></li>
{% endif %}
{% endfor %}
{#
1.下一页信息判断逻辑
1). 判断是否有下一页信息
2). 如果有,创建链接
3). 如果没有,该链接设为不可点击的链接
2. 使用的方法:
1). dataObj.has_next:判断用户是否有下一页 ----> 如果有,返回True;如果没有,返回False;
2).dataObj.next_num:获取下一页的页数编号;
#}
{% if todoPageObj.has_next %}
<li>
<a href="{{ url_for('list', page=todoPageObj.next_num) }}" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% else %}
<li class="disabled">
<a href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% endblock %}
7.在视图函数 view.py 中实现业务逻辑:
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
# 判断用户是否存在?
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/<int:id>/', 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并不能获取用户更新后提交的表单内容
# 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/<int:id>/')
@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/<int:page>/')
@is_login
def list(page=1):
# 任务显示需要分页
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/<int:id>/')
@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/<int:id>')
@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'))