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

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.技术分析

为什么选择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组件,根据这些组件,可以快速的搭建一个漂亮、功能完备的网站。其中包括以下组件:下拉菜单、按钮组、按钮下拉菜单、导航、导航条、路径导航、分页、排版、缩略图、警告对话框、进度条、媒体对象等

项目内容

基于Flask的任务清单管理系统_第1张图片
init.py

from flask import Flask

from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_bootstrap import Bootstrap
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)

forms.py(web表单)

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, SelectField, DateField
from wtforms.validators import DataRequired, Email, Length, EqualTo, ValidationError

# 注册表单
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):
        # field.data==username表单提交的内容
        u = User.query.filter_by(username=field.data).first()
        if u:
            raise ValidationError('用户名%s已经注册' % (u.username))  # username 数据库中所存数据

    def validate_email(self, field):
        u = User.query.filter_by(email=field.data).first()
        if u:
            raise ValidationError('邮箱%s已经注册' % (u.email))


# 登录表单
class LoginForm(FlaskForm):
    username = StringField(
        label='用户名',
        validators=[
            DataRequired(),
        ]
    )
    password = PasswordField(
        label='密码',
        validators=[
            DataRequired(),

        ]
    )
    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 = DateField(
        label='任务终止日期',
    )
    submit = SubmitField(
        label='添加任务'
    )

#编辑任务表单
class EditTodoForm(TodoForm):
    submit = SubmitField(
        label='编辑任务'
    )

models.py(数据库表模板)

from app import db

from werkzeug.security import generate_password_hash, check_password_hash

from datetime import datetime

#用户表
class User(db.Model):
    # __tablename__='user' 可以使用该方法对表重命名
    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.utcnow())    #账户创建时间
    #1.User添加属性todos    2.Todo添加属性user
    todos = db.relationship('Todo',backref='user')
    categories = db.relationship('Category',backref='user')
    @property
    def password(self):
        raise AttributeError('密码属性不可读')

    @password.setter
    def password(self,password):
        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)    #任务状态
    #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())    #任务创建时间
    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')
    user_id = db.Column(db.Integer,db.ForeignKey('user.id'))

    def __repr__(self):
        return ''%(self.name)

views.py(视图函数)

import json
from functools import wraps

from flask import flash, redirect, url_for, render_template, session, request

from app import app, db
from app.forms import LoginForm, RegisterForm, AddTodoForm, EditTodoForm
from app.models import User, Todo


#用来判断用户是否登录的装饰器
def is_login(f):
    @wraps(f)
    def wrapper(*args,**kwargs):
        #判断session对象中是否有session['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 'index'

@app.route('/login/',methods=['POST','GET'])
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('/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)

"""
当有多个装饰器的时候,从下到上调用
真实的warpper内容是从上到下执行的
"""
@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=['POST','GET'])
@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=['POST','GET'])
@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并不能获取用户更新后提交的表单内容
        """
        error:
            content = form.content.data
            category_id = form.category.data
        """
        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中设置PER_PAGE
    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'))
# 通过echarts在前端显示任务完成与未完成的比例
@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
                           })

@app.route('/newShowTodo/')
@is_login
def newShowTodo():
    return render_template('todo/new_showtodo.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]
    }
    #解决中文编码问题
    return json.dumps(info,ensure_ascii=False)


#CPU占有率显示
@app.route('/cpu/')
def get_cpu():
    import psutil
    cpuInfo = json.dumps({'CPU占有率',psutil.cpu_percent()},
                         ensure_ascii=False)
    return cpuInfo

config.py(配置文件)

#数据库配置
SQLALCHEMY_DATABASE_URI = 'mysql://root:redhat@localhost/TodoProject'
SQLALCHEMY_TRACK_MODIFICATIONS = True
#缓存安全设置,防止跨战请求伪造
SECRET_KEY = 'westos'
#分页中每一页的数量
PER_PAGE=5

manage.py(应用)

from app import manager,db
from flask_migrate import MigrateCommand
from app.models import Category, Todo,User
from app.views import *


@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()

static(静态文件):
css

# 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官网下载

templates(html页面):
根据相应视图函数的功能编写适合的html页面,可通过bootstrap中文文档上的模板制作美观的页面。

Flask-Moment本地化日期和时间

1. 为什么使用Flask-Moment?

如果Web程序的用户来自世界各地,那么就思考如何让Web的世界和当地时间一致。
服务器需要统一时间单位,这和用户所在的地理位置无关,所以一般会使用协调
时间时(Coordinated Universal Time,UTC)。但是对于用户来说他们
想看到的是自己所在的当地时间,而且使用当地惯用的格式。

2. Flask-Moment实现原理?

一个优雅的解决方法就是把时间单位发送给Web浏览器,转换成当地时间,
然后渲染。Web浏览器可以更好的更成这一任务,因为他们能获得电脑中的
时区和区域设置。

有一个使用JavaScript开发的优秀客户端开源代码库,名为moment.js,
它可以在浏览器中渲染日期和时间。Flask-Moment是一个Flask程序扩展,
能把moment.js集成到Jinja2模板中。

3. 具体代码?

3-1. 模板的编写

  • 除了moment.js.Flask_Moment还依赖jquery.js,要在HTML文档的引入这连个文件,这样可以选择使用哪个版本,
    也可以使用扩展提供的辅助函数,从内容分发网络中引入通过测试的加新内容,必须使用JinJa2提供的super()函数。比如可以这样在基模板中的scripts块中引入这个库。

{% block script %}
{{ super()}}
{{ monent.include_moment()}}
{% endblock %}



3-2. 编辑视图函数

为了处理时间,Flask-Moment向模板开放了monent类。
所以代码可以把变量current_time(也就是UTC时间传给模板处理)传入模板进行渲染。就这样解决了本地时间问题

@app.route('/')
def index():
    return render_template('index.html',
                           current_time=datetime.utcnow())

3-3. 编辑页面逻辑

The local date and time is {{ moment(current_time).format('LLL') }}.

That was {{ moment(current_time).fromNow(refresh=True) }}.

4. 参考资料

Flask-Moment 实现了 moment.js 中的 format() 、 fromNow() 、 fromTime() 、 calendar() 、 valueOf()和 unix() 方法。你可查阅文档(http://momentjs.com/docs/#/displaying/)学习 moment.js 提供的全部格式化选项。

基于Flask和pyecharts的图形可视化

1.Flask整合pyecharts的第一种方式

2.Flask整合Echarts库的第2种方式

Flask 是python web开发的微框架,Echarts酷炫的功能主要是javascript起作用,将两者结合起来,发挥的作用更大。

2-1. 静态请求实现步骤

  • 1).去官网下载Echarts,如下图所示,下载完整版
  • 2). 引入看Echarts官网文档的教程

2-2. 动态请求步骤: 使用json和ajax请求

  • 什么是ajax?
    AJAX = Asynchronous
    JavaScript and XML(异步的 JavaScript 和 XML)。

AJAX 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX 可以使网页实现异步更新。
这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

  • 什么是json?
    JSON:JavaScript 对象表示法(JavaScript Object Notation)。JSON 是存储和交换文本信息的语法。类似 XML。
@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);

                }
            })
        })

你可能感兴趣的:(笔记)