基于Flask的任务清单管理系统

文章目录

    • 1.目标
    • 2.项目介绍
    • 3.项目效果
    • 4.技术分析
    • 5.项目实现
        • 创建项目
        • 2. 项目实现

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,并创建一个新的虚拟环境:

基于Flask的任务清单管理系统_第1张图片
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 "" % (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 "" % (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 "" % (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="[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()
	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//')
def todo_edit(id):
    return "todo_edit %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'

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() }}
    
{% endblock %}

{% block scripts %}
    {{ super() }}
    {# 实质是导入moment.js库的,通过Flask-moment集成了起来 #}
    {{ moment.include_moment() }}
    
{% endblock %}

{% block navbar %}
    


    {# 让每个页面都可以获取闪现信息闪现 #}
    {% for item in get_flashed_messages() %}

        
    {% endfor %}
{% endblock %}
  • 注册页面(register.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}注册页面{% endblock %}
{% block content %}
    
{{ wtf.quick_form(form) }}
{% endblock %}
  • 登录页面(login.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}登录页面{% endblock %}
{% block content %}
    
{{ wtf.quick_form(form) }}
{% endblock %}
  • 添加任务(todo / add_todo.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}添加任务{% endblock %}
{% block content %}
    
{{ wtf.quick_form(form) }}
{% endblock %}
  • 编辑任务(todo / edit_todo.html)
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block title %}添加任务{% endblock %}
{% block content %}
    
{{ wtf.quick_form(form) }}
{% endblock %}
  • 查看任务(todo / list_todo.html)
{% extends 'base.html' %}

{% block title %}任务显示{% endblock %}

{% block content %}
    
{% for todo in todoPageObj.items %} {% if todo.status %} {% else %} {% endif %} {# #} {% endfor %}
任务内容 任务状态 任务分类 所属用户 创建时间 操作
{{ todo.content }} {{ todo.content }} {% if todo.status %} {% else %} {% endif %} {{ todo.category.name }} {{ todo.user.username }}{{ moment(todo.add_time).format('L') }}{{ moment(todo.add_time).fromNow(refresh=True) }} 编辑 删除
{% 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//', 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//')
@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):
    # 任务显示需要分页
    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'))


你可能感兴趣的:(基于Flask的任务清单管理系统)