本项目将学习 Mariadb 作为数据库后端,Bootstrap 作为前端的技术栈,并实现一个清单应用。从中我们可以学习 Flask Web 应用框架,及 Mariadb 关系型数据库和 BootStrap web开发框架。
本应用修改自 TodoMVC 的 todo list 应用,使用 Mariadb 作为数据库后端,Bootstrap 作为前端的 Flask 应用。先给它起个好听的名字吧,方便之后称呼。
todo list => (自定义,随便起名称) => todoest
就像一般的 todo list 应用一样,todoest 实现了以下功能:
为什么选择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组件,根据这些组件,可以快速的搭建一个漂亮、功能完备的网站。其中包括以下组件:下拉菜单、按钮组、按钮下拉菜单、导航、导航条、路径导航、分页、排版、缩略图、警告对话框、进度条、媒体对象等
首先在mysql里新建数据库
create database Todo DEFAULT CHARSET utf8
config.py
SQLALCHEMY_DATABASE_URI = 'mysql://root:redhat@localhost/Todo'
SQLALCHEMY_TRACK_MODIFICATIONS = True
manage.py
from app import manager
if __name__ == '__main__':
manager.run()
init.py
from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
import pymysql
pymysql.install_as_MySQLdb()
app = Flask(__name__)
app.config.from_pyfile('../config.py')
db = SQLAlchemy(app)
manager = Manager(app)
models.py
数据库:
存储三类 用户/Todo/Todo的类型
"""
# 存储数据库相关的操作
"""
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)
# 初始化,用于测试数据库,实际代码不用写,因为把初始化数据库写在models.py文件中更为方便
def init():
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()
if __name__ == '__main__':
init()
view.py–视图函数(先实现登陆注册页面,再写出todo页面框架)
from app import app, db
# 网站首页
from app.forms import RegisterForm, LoginForm
from flask import render_template, flash, redirect, url_for, session
from app.models import User
@app.route('/')
def index():
return 'index'
# 注册页面
@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/')
def logout():
session.pop('user_id', None)
session.pop('user', None)
return redirect(url_for('login'))
# 添加任务
@app.route('/todo/add/')
def todo_add():
return 'todo_add'
# 编辑任务
@app.route('/todo/edit//')
def todo_modify(id):
return "todo_modify %s" % (id)
# 删除任务
@app.route('/todo/delete//')
def todo_delete(id):
return "todo_delete"
# 查看任务
@app.route('/todo/list/')
@app.route('/todo/list/')
def list(page):
return "list"
# 修改任务状态为完成
@app.route('/todo/done//')
def done(id):
return 'done'
# 修改任务状态为未完成
@app.route('/todo/undo/')
def undo(id):
return 'undo'
forms.py–表单文件
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, ValidationError
from wtforms.validators import DataRequired, Email, Length, EqualTo
# 注册表单
from app.models import User
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="登录"
)
css样式:
/static/css/main.css 存储css样式 用于继承
.navbar {
font-size: 130%;
background: whitesmoke;
margin-top: 10px;
padding-top: 5px;
box-shadow: 2px 2px 2px 2px lightgray;
height: 60px;
}
templates
base.html --基模板(copy之前的flask注册页面)
这里继承的是bootstrap的base模板(之前讲过继承bootstrap框架)
{% extends 'bootstrap/base.html' %}
{% block styles %}
{# 先继承父类的css样式导入 #}
{{ super() }}
{% endblock %}
{% block navbar %}
{#让每个页面都可以获取闪现信息闪现#}
{% for item in get_flashed_messages() %}
{{ item }}
{% endfor %}
{% endblock %}
login.html – 登陆页面
wtf.quick_form(form) # 快速生成表单
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}登录页面{% endblock %}
{% block content %}
{% endblock %}
register.html --注册页面与login.html相似
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}注册页面{% endblock %}
{% block content %}
{% endblock %}
config.py
SQLALCHEMY_DATABASE_URI = 'mysql://root:redhat@localhost/Todo'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = 'westos'
manage.py
from app import manager, db
from app.views import *
# 添加数据库操作的命令信息;
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()
if __name__ == '__main__':
manager.run()
class AddTodoForm(FlaskForm):
content = StringField(
label='任务内容',
validators=[DataRequired()]
)
category = SelectField(
label='任务类型',
coerce=int, # 存的是id整形
choices=[(item.id,item.name) for item in Category.query.all()]
)
submit = SubmitField(
label='添加任务',
)
在views.py视图函数里添加add视图函数
# 添加任务
@app.route('/todo/add/')
def todo_add():
form = AddTodoForm()
if form.validate_on_submit():
pass
return render_template('todo/add_todo.html',form=form)
在template 下新建todo目录,把login登陆的html复制过来简单修改
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}添加任务{% endblock %}
{% block content %}
添加任务
{{ wtf.quick_form(form) }}
{% endblock %}
运行python manager.py runserver
forms.py的@app.route(’/todo/add/’),post方法补充完整
@app.route('/todo/add/',methods=['GET','POST'])
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/list/')
@app.route('/todo/list/')
def list(page=1):
# 任务需要分页显示
todoPageObj= Todo.query.paginate(page,per_page=app.config['PER_PAGE'])
return render_template('todo/list_todo.html',todoPageObj=todoPageObj)
list_todo.html
{% extends 'base.html' %}
{% block title %}
任务显示
{% endblock %}
{% block content %}
任务显示
任务内容
任务状态
任务分类
所属用户
{% for todo in todoPageObj.items %}
{{ todo.content }}
{{ todo.status }}
{{ todo.category. name}}
{{ todo.user.username}}
{% endblock %}
在config中添加每页的页数per_page
SQLALCHEMY_DATABASE_URI = 'mysql://root:redhat@localhost/Todo'
SQLALCHEMY_TRACK_MODIFICATIONS = True
SECRET_KEY = 'westos'
PER_PAGE = 5
运行一下:
todo是一个可以交互的页面,所以我们需要加上按钮可以删除和编辑,以及对勾和叉叉
bootstrap按钮的css样式
任意选择几个按钮样式复制到任务列表的html代码里,按钮样式举例:
list_todo.html
{% extends 'base.html' %}
{% block title %}
任务显示
{% endblock %}
{% block content %}
{% endblock %}
再网页上找对勾和叉叉的样式图标显示
在状态status这里添加判断:
{% extends 'base.html' %}
{% block title %}
任务显示
{% endblock %}
{% block content %}
{% endblock %}
在页面上显示分页:
要让分页栏显示在表格下面,所以写在table标签后面
{# # Day36 templates/list.html#}
# 删除任务
@app.route('/todo/delete//')
def todo_delete(id):
todo = Todo.query.filter_by(id=id).first()
db.session.delete(todo)
db.session.commit()
return redirect(url_for('list'))
修改list_to.html里删除按钮,a标签里加上连接地址
删除
将任务内容和任务类型作为一个任务的基类,其他任务的类全继承于这个基类
forms.py
# 关于任务的基类
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="编辑任务",
)
当get方法时的视图函数
# 编辑任务
@app.route('/todo/edit//',methods=['GET','POST'])
def todo_edit(id):
form = EditTodoForm()
if form.validate_on_submit():
pass
return render_template('todo/edit_todo.html',form=form)
edit_todo.html修改之前添加任务的页面
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}编辑任务{% endblock %}
{% block content %}
编辑任务
{{ wtf.quick_form(form) }}
{% endblock %}
然后把list_todo.html里面编辑按钮的链接加上
编辑
测试一下编辑按钮能否跳转
最后完善一下post方法的视图函数:
# 编辑任务
@app.route('/todo/edit//', methods=['GET', 'POST'])
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)
修改任务状态的视图函数
1.状态为对勾
# 修改任务状态为完成
@app.route('/todo/done//')
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'))
修改html代码
{% if todo.status %}
{% else %}
{% endif %}
# 修改任务状态为未完成
@app.route('/todo/undo/')
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'))
{% if todo.status %}
{% else %}
{% endif %}
为了更有好显示,可以先判断状态,把状态为叉的任务内容划去后再显示:用del标签
{% for todo in todoPageObj.items %}
{% if todo.status %}
{{ todo.content }}
{% else %}
{{ todo.content }}
{% endif %}
{% if todo.status %}
{% else %}
{% endif %}
{{ todo.category. name }}
{{ todo.user.username }}
{# 按钮#}
编辑
删除
{% endfor %}
/home/kiosk/Documents/python/python0310/day37_TodoProject
0316 P156