为了账号安全,请及时绑定邮箱和手机立即绑定

Flask构建弹幕微电影网站-博客小项目学完flask基础

2018.07.21 23:39 797浏览

已上线演示地址: http://movie.mtianyan.cn
项目源码地址:https://github.com/mtianyan/movie_project

实战小项目介绍

  1. 项目介绍

  2. 项目演示

  3. 开发思路

实现用户注册,用户登录,退出登录,添加文章
文章分页列表,修改文章,删除文章等功能

开发思路:

mark

创建路由,视图,模板,静态文件

路由(route):

@app.route("/login/", methods=["GET","POST"]) # 用户登录@app.route("/logout/", methods=["GET"]) # 用户退出@app.route("/register/", methods=["GET","POST"]) # 用户注册@app.route("/art/add", methods=["GET","POST"]) # 发布文章@app.route("/art/edit/<int:id>/", methods=["GET","POST"]) # 编辑文章@app.route("/art/list/", methods=["GET"]) # 文章列表@app.route("/art/del/<int:id>/", methods=["GET"]) # 删除文章

路由就是我们访问网站时需要在浏览器中输入的一串地址。

视图(views):

login # 用户登录logout # 用户退出register # 用户注册art_add # 发布文章art_edit # 编辑文章art_list # 文章列表art_del # 删除文章

通过后端与前端的结合,通过视图书写一些后端的逻辑。

模板(templates):

login.html # 用户登录register.html # 用户注册art_add.html # 发布文章art_edit.html # 编辑文章art_list.html # 文章列表

html展示页面

静态文件(static):

css # 层叠样式表js # javascript脚本ue # 百度ueditor富文本编辑器

为项目创建自己的虚拟环境

mkvirtualenv -p=D:\softEnvDown\Anaconda2\envs\py36\python.exe flaskgetstarted
pip install flask

创建一个纯python项目,选择我们刚才的虚拟环境。

mark

将项目创建为如上图所示。

views.py:

# encoding: utf-8__author__ = 'mtianyan'__date__ = '2018/2/12 0012 00:34'from flask import  Flask

app = Flask(__name__)# login # 用户登录# logout # 用户退出# register # 用户注册# art_add # 发布文章# art_edit # 编辑文章# art_list # 文章列表# art_del # 删除文章

下面我们开始定义这些视图

# encoding: utf-8__author__ = 'mtianyan'__date__ = '2018/2/12 0012 00:34'from flask import Flask, render_template, redirect, url_for

app = Flask(__name__)# login 用户登录@app.route("/login/", methods=["GET", "POST"])  # 用户登录def login():    # 返回渲染模板
    return render_template("login.html")# logout 用户退出(302跳转到登录页面)@app.route("/logout/", methods=["GET"])  # 用户退出def logout():    # 重定向到指定的视图对应url,蓝图中才可以使用
    # return redirect(url_for("app.login"))

    # 直接跳转路径
    return  redirect("/login/")# register 用户注册@app.route("/register/", methods=["GET", "POST"])  # 用户注册def register():    return render_template("register.html")# art_add 发布文章@app.route("/art/add/", methods=["GET", "POST"])  # 发布文章\def art_add():    return render_template("art_add.html")# art_edit 编辑文章# 传入整型id参数@app.route("/art/edit/<int:id>/", methods=["GET", "POST"])  # 编辑文章def art_edit(id):    return render_template("art_edit.html")# art_list 文章列表@app.route("/art/list/", methods=["GET"])  # 文章列表def art_list():    return render_template("art_list.html")# art_del 删除文章@app.route("/art/del/<int:id>/", methods=["GET"]) # 删除文章def art_del(id):    return redirect("/art/list")if __name__ == "__main__":
    app.run(debug=True, host="127.0.0.1", port=8080)

可能遇到的访问错误:

builtins.TypeErrorTypeError: art_del() got an unexpected keyword argument 'id'

你的方法没有将url中的参数接收进来。

定义视图使用函数方法,定义路由使用装饰器方法。
render_template()redirect()302跳转

  • url中包含可变参数,并在视图函数中接收

  • 如何启动:name main app.run

mark

Jinja2模板语法

  1. 继承

{% extends "父模板路径" %}
  1. 数据块(页面块)

{% block 块名 %} ... {% endblock %}
  1. 路由生成

{{ url_for("模块名.视图名") }}
  1. 静态文件加载

静态文件目录

{{ url_for('static', filename='静态文件路径')}}
  1. 循环语句:

{% for 条件 %} ... {% endfor %}
  1. 条件语句

显示什么不显示什么

{% if 条件 %} ... {% endif %}

编写用户注册登录界面(Bootstrap)

Bootstrap

快速入门。

https://getbootstrap.com/docs/4.0/getting-started/download/

https://github.com/wangfupeng1988/wangEditor/releases

下载富文本编辑器将其中的release拷贝进目录并改名字

mark

mark

<!doctype html><html lang="zh-cn"><head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登录页面</title></head><body></body></html>

自己补全doctype html zh-cn

自行下载jquery.slim.min.js
自行下载 popper.min.js

百度在各自官网即可下载

<link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='css/bootstrap.min.css') }}"><script src="{{ url_for('static',filename='js/jquery-3.3.1.slim.min.js') }}"></script><script src="{{ url_for('static',filename='js/popper.min.js') }}"></script><script src="{{ url_for('static',filename='js/bootstrap.min.js') }}"></script>

注意后面三个js的加载顺序。以及css(head中) js(body中)

<div class="container" style="margin-top: 100px">
    <div class="row">
        <div class="col-md-12">
            <h3 class="text-center">文章管理系统</h3>
        </div>
    </div></div>

定义容器,内含行,定义栅格系统占12个位置。
h3标签使之变大,text-center居中,style margintop 距离上面100px

        <div class="col-md-6 offset-md-3">            <div class="card-header">登录</div>
            <div class="card-body">                <form>
                    <div class="form-group">
                        <label>账号</label>
                        <input type="text" class="form-control" placeholder="请输入账号">
                    </div>
                    <div class="form-group">
                        <label>密码</label>
                        <input type="password" class="form-control" placeholder="请输入密码">
                    </div>
                </form>
            </div>
        </div>

使用6个栅栏位置,并使用(12-6)/2进行居中操作。

  • 使用card-header card-body定义卡片头与内容

  • form group 中包含 label 和 input

定义登录

<a href="/register/">没有账号?前往注册</a><br>
 <a class="btn btn-primary" href="/art/list">登录</a>

新建user_base.html

将除了表单的都拷过来

mark

把 login中的除form删除掉

mark

此时访问登录的字不见了。因为title没有值传递过来。

return render_template("login.html", title ="登录")

注册页面

mark

注册我们要引入自定义的js。

所以在base中在定义一个数据块,然后复写它。

mark

http://www.bootcdn.cn/holder/

<script src="https://cdn.bootcss.com/holder/2.9.4/holder.min.js"></script>

引入holder.min.js图片占位。

传递注册title

<form>
                    <div class="form-group">
                        <label>账号</label>
                        <input type="text" class="form-control" placeholder="请输入账号">
                    </div>
                    <div class="form-group">
                        <label>密码</label>
                        <input type="password" class="form-control" placeholder="请输入密码">
                    </div>
                    <div class="form-group">
                        <label>确认密码</label>
                        <input type="password" class="form-control" placeholder="请确认密码">
                    </div>
                    <div class="form-group">
                        <label>验证码</label>
                        <input type="text" class="form-control" placeholder="请输入验证码">
                        <img data-src="holder.js/180x50" style="margin-top: 6px">
                    </div>
                    <a href="/login/">已有账号!前往登录</a>
                    <br>
                    <a class="btn btn-primary" href="/login/">注册</a></form>

编写文章编辑列表页面(百度ueditor)

头部和菜单是公共的部分:

<!doctype html><html lang="zh-cn"><head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>文章管理系统</title>
    <link rel="stylesheet" type="text/css" href="{{ url_for('static',filename='css/bootstrap.min.css') }}">{% block css %} {% endblock %}</head><body>{% block js %} {% endblock %}<script src="{{ url_for('static',filename='js/jquery-3.3.1.slim.min.js') }}"></script><script src="{{ url_for('static',filename='js/popper.min.js') }}"></script><script src="{{ url_for('static',filename='js/bootstrap.min.js') }}"></script><script src="https://cdn.bootcss.com/holder/2.9.4/holder.min.js"></script></body></html>

首先将文件都引入。

定义导航。

使用bootstrap的nav标签

mark

<nav class="navbar navbar-light" style="background-color: #062c33">
    <div class="container">
        <a class="navbar-brand" style="color: #ffffff"> 文章管理系统</a>
        <a class="btn btn-outline-warning" href="/login/">账号名称 -> 退出</a>
    </div></nav>

定义左侧菜单:

<div class="container" style="margin-top: 6px">
    <div class="row">
        <div class="col-md-3">
            <div class="list-group">
                <a href="/art/add/" class="list-group-item list-group-item-action">发布文章</a>
                <a href="/art/list/" class="list-group-item list-group-item-action">文章列表</a>
            </div>
        </div>
        <div class="col-md-9"></div>
    </div></div>

其中左侧菜单占3个栅栏,剩下的9个归文章所有。

        <div class="col-md-9">            <div class="card">
                <div class="card-header">文章列表</div>
                 <div class="card-body">
                     <table class="table table-bordered">
                         <thead>
                         <tr>
                             <th>标题</th>
                             <th>分类</th>
                             <th>封面</th>
                             <th>作者</th>
                             <th>发布时间</th>
                             <th>管理操作</th>
                         </tr>
                         </thead>
                         {% for v in range(1,11) %}                     <tr>
                         <td>mtianyan编程之旅</td>
                         <td>python</td>
                         <td><img data-src="holder.js/75x40"></td>
                         <td>mtianyan</td>
                         <td>2018-02-12 12:00:00</td>
                         <td>
                             <button class="btn btn-sm btn-outline-success">编辑</button>
                             <button class="btn btn-sm btn-outline-danger">删除</button>
                         </td>
                     </tr>
                     {% endfor %}                     </table>
                 </div>
            </div>
        </div>

成型效果:

mark

分页:

 <nav>
                         <ul class="pagination">
                             <li class="page-item"><a class="page-link" href="#">上一页</a></li>
                             <li class="page-item"><a class="page-link" href="#">1</a></li>
                             <li class="page-item"><a class="page-link" href="#">2</a></li>
                             <li class="page-item"><a class="page-link" href="#">3</a></li>
                             <li class="page-item"><a class="page-link" href="#">4</a></li>
                             <li class="page-item"><a class="page-link" href="#">下一页</a></li>
                         </ul>
                     </nav>

mark

创建art_base页面作为爸爸页面

将刚才的art_list页面内容全部拷贝。然后把可变部分9栏删除。用block content代替

mark

原本list文件中只保留可变部分9栏,继承art_base

mark

mark

可以看到如上图内容没有居中显示。我们可以右键检查打开查看元素

.table td,.table th {    padding: .75rem;    vertical-align: top;    border-top: 1px solid #dee2e6;
}

改为垂直居中

    .table td,    .table th{        vertical-align: middle;
    }

list中重写css block

mark

发布文章

  • 首先继承art_base

mark

菜单分离出来使用include引入。

mark

mark

  • 实现active

添加block js代码

<script>
    $(document).ready(function () {
        $("#m2").addClass("active");
    });</script>

列表页为m2 添加页为m1

可能遇到的不生效的错误

document是使用jQuery实现的,所以我们的blockjs一定要在jQuery引入之后

mark

views中传入标题:

def art_list():
    return render_template("art_list.html",title="文章列表")

art_add 同理可得

然后在art_list和art_add中使用参数。

mark

开始编写art_add 的content

<form>
    <div class="form-group">
        <label>标题</label>
        <input type="text" class="form-control" placeholder="请输入标题!">
    </div>
    <div class="form-group">
        <label>分类</label>
        <select class="form-control">
            <option value="1">科技</option>
            <option value="2">搞笑</option>
            <option value="3">军事</option>
        </select>
    </div>
    <div class="form-group">
        <label>封面</label>
        <input type="file" class="form-control-file"style="margin-left: 2px;">
        <img data-src="holder.js/300x160" style="margin-top: 6px;">
        <a class="btn btn-primary" style="margin-top: 6px;">上传封面</a>
    </div>
    <div class="form-group">
         <label>内容</label>
        <textarea class="form-control"></textarea>
    </div></form>

此时我们的内容还并不是那个富文本,因此我们这时候需要将富文本集成。

前往官网下载php版本拷入项目目录

mark

加载ue需要的js


mark

给我们的内容一个id=content

    <div class="form-group">
         <label>内容</label>
        <textarea class="form-control" id="content"></textarea>
    </div>

然后在js中

    var ue = UE.getEditor("content");

将原来的textera的class去掉。修复样式

    <div class="form-group">
         <label>内容</label>
        <textarea id="content" style="height: 300px;"></textarea>
    </div>
    <button type="button" class="btn btn-primary">发布文章</button>

使用sqlalchemy定义数据表

操作mysql

ssh root@192.168.0.7mysql -uroot -p
\s

mark

可以看到当前的状态。

vim /etc/my.cnf

mark

mark

修改mysql编码。

mark

然后重启服务service mysql restart。再次查看。

mark

四个utf8表示设置成功。

centos: 编辑vim /ect/my.cnf

创建数据库 artcms

原生方法与model ORM方式对比

创建artcms.sql并思考我们需要的字段

/*
用户表
0. id编号
1. 账号
2. 密码
3. 注册时间
 */CREATE TABLE if NOT EXISTS user(
  id int unsigned not null auto_increment key comment "主键ID",
  account varchar(20) not null comment "账号",
  pwd  varchar(100) not null comment "密码",
  addtime datetime not null comment "注册时间")engine=InnoDB DEFAULT charset=utf8 comment "用户";/*
文章表
0. id编号
1. 标题
2. 分类
3. 作者
4. 封面
5. 内容
6. 发布时间
 */CREATE TABLE if NOT EXISTS article(
  id int unsigned not null auto_increment key comment "主键ID",
  title varchar(100) not null comment "标题",
  category  tinyint unsigned not null comment "分类",
  user_id int unsigned not null comment "作者",
  logo varchar(100) not null comment "封面",
  content mediumtext not null comment "文章",
  addtime datetime not null comment "发表时间")engine=InnoDB DEFAULT charset=utf8 comment "文章";

操作数据库需要使用原生语句。以后不用mysql 数据库迁移存在问题。

安装mysql client

lsof -i:3306pip install Flask-SQLAlchemy
# encoding: utf-8__author__ = 'mtianyan'__date__ = '2018/2/12 0012 00:35'from flask import Flaskfrom flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql://root:tp158917@127.0.0.1:3306/artcms"# 如果设置成 True (默认情况),Flask-SQLAlchemy 将会追踪对象的修改并且发送信号。app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = True# 绑定app至SQLAlchemydb = SQLAlchemy(app)"""
用户模型
0. id编号
1. 账号
2. 密码
3. 注册时间
"""class User(db.Model):
    __tablename__ = "user"
    id = db.Column(db.Integer, primary_key=True)  # 编号id
    account = db.Column(db.String(20), nullable=False)  # 账号非空
    pwd = db.Column(db.String(100), nullable=False)  # 密码非空
    add_time = db.Column(db.DateTime, nullable=False)  # 注册时间

    # 查询时的返回
    def __repr__(self):
        return "<User %r>" % self.account"""
文章模型
0. id编号
1. 标题
2. 分类
3. 作者
4. 封面
5. 内容
6. 发布时间
"""class Article(db.Model):
    __tablename__ = "article"
    id = db.Column(db.Integer, primary_key=True)  # 编号id
    title = db.Column(db.String(100), nullable=False)  # 标题非空
    category = db.Column(db.Integer, nullable=False)  # 编号id
    user_id = db.Column(db.Integer, nullable=False)  # 作者
    logo = db.Column(db.String(100), nullable=False)  # 封面
    content = db.Column(db.Text, nullable=False)  # 内容
    add_time = db.Column(db.DateTime, nullable=False)  # 发布时间

    # 查询时的返回
    def __repr__(self):
        return "<Article %r>" % self.title
# 执行创建表语句if __name__ == "__main__":
    db.create_all()

执行时警告

 Warning: (1366, "Incorrect string value: '\\xD6\\xD0\\xB9\\xFA\\xB1\\xEA...' for column 'VARIABLE_VALUE' at row 480")

windows下暂无解决方案。把mysql所有编码都设为utf8也不行。连接一个linux下数据库没有该警告。

使用wtforms定义表单

输入框的集合被我们统称为表单,对于表单进行统一的管理,对于表单数据进行统一的验证。

集中化的管理表单: wtforms

安装Flask-WTF,并定义登录的表单

# encoding: utf-8__author__ = 'mtianyan'__date__ = '2018/2/12 0012 00:34'from flask_wtf import FlaskFormfrom wtforms import StringField, PasswordField, SubmitField"""
登录表单:
1. 账号
2. 密码
3. 登录按钮
"""class LoginForm(FlaskForm):
    account = StringField(
        label=u"账号",
        validators=[],
        description=u"账号",
        render_kw={            "class": "form-control",            "placeholder": "请输入账号!"
        }
    )
    pwd = PasswordField(
        label=u"密码",
        validators=[],
        description=u"密码",
        render_kw={            "class": "form-control",            "placeholder": "请输入密码!"
        }
    )
    submit = SubmitField(        u"登录",
        render_kw={            "class": "btn btn-primary"
        }
    )

在views中使用loginform

from forms import LoginForm# login 用户登录@app.route("/login/", methods=["GET", "POST"])  # 用户登录def login():
    form = LoginForm()    # 返回渲染模板
    return render_template("login.html", title="登录", form=form)

前往login页面进行改造

mark

直接去掉图中的代码改为

mark

此时点击退出登录

builtins.KeyError
KeyError: 'A secret key is required to use CSRF.'

views中定义:

app.config["SECRET_KEY"] = "12345678"

然后可以正确执行。

定义注册表单并替换。

"""
注册表单:
1. 账号
2. 密码
3. 确认密码
4. 验证码
5. 注册按钮
"""class RegisterForm(FlaskForm):
    account = StringField(
        validators=[],
        description=u"账号",
        render_kw={            "class": "form-control",            "placeholder": "请输入账号"
        }
    )
    pwd = PasswordField(
        validators=[],
        description=u"密码",
        render_kw={            "class": "form-control",            "placeholder": "请输入密码"
        }
    )
    re_pwd = PasswordField(
        validators=[],
        description=u"确认密码",
        render_kw={            "class": "form-control",            "placeholder": "请确认密码"
        }
    )
    captcha = StringField(
        validators=[],
        description=u"验证码",
        render_kw={            "class": "form-control",            "placeholder": "请输入验证码"
        }
    )
    submit = SubmitField(        u"注册",
        render_kw={            "class": "btn btn-primary"
        }
    )
# register 用户注册@app.route("/register/", methods=["GET", "POST"])  # 用户注册def register():
    form = RegisterForm()    return render_template("register.html", title ="注册", form=form)

mark

发布文章的表单

"""
发布文章表单:
1. 标题
2. 分类
3. 封面
4. 内容
5. 发布文章按钮
"""class ArticleAddForm(FlaskForm):
    title = StringField(
        validators=[],
        description=u"标题",
        render_kw={            "class": "form-control",            "placeholder": "请输入标题"
        }
    )    # 强制类型为整型
    category = SelectField(
        validators=[],
        description=u"分类",
        choices=[(1, u"科技"), (2, u"搞笑"), (3, u"军事")],
        default=3,
        coerce=int,
        render_kw={            "class": "form-control"
        }
    )
    logo = FileField(
        validators=[],
        description=u"封面",
        render_kw={            "class": "form-control-file",
        }
    )
    content = TextAreaField(
        validators=[],
        description=u"内容",
        render_kw={            "style": "height:300px;",            "id": "content"
        }
    )
    submit = SubmitField(        u"发布文章",
        render_kw={            "class": "btn btn-primary"
        }
    )

view中实例化并传到html中去。

# art_add 发布文章@app.route("/art/add/", methods=["GET", "POST"])  # 发布文章\def art_add():
    form = ArticleAddForm()    return render_template("art_add.html", title="发布文章", form=form)

mark

实现用户注册功能

  1. 进行数据验证。

定义验证规则。

forms.py 中:

from wtforms.validators import DataRequired, EqualTo, ValidationError

分别是字段必须存在,密码相等,自定义错误信息。

        validators=[
            DataRequired(u"账号不能为空")
        ],

此处照猫画虎环节:自行为其他非空字段也填上验证和信息。

mark

    re_pwd = PasswordField(
        validators=[
            DataRequired(u"确认密码不能为空"),
            EqualTo('pwd', message=u"两次输入密码不一致")
        ],
        )

去视图中进行调用

from forms import LoginForm, RegisterForm, ArticleAddForm

然后进行验证逻辑:

def register():
    form = RegisterForm()    if form.validate_on_submit():
        data = form.data

在form标签中加入crsf token验证

mark

mark

设置提交方式为post,定义错误信息的显示。

mark

其他也同理设置。

保存数据;

def register():
    form = RegisterForm()    if form.validate_on_submit():
        data = form.data        # 保存数据
        user = User(
            account=data["account"],            # 对于pwd进行加密
            pwd=generate_password_hash(data["pwd"]),
            add_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        )
        db.session.add(user)
        db.session.commit()        # 定义一个会话的闪现
        flash("注册成功, 请登录", "ok")        return redirect("/login/")    return render_template("register.html", title="注册", form=form)

遇到的错误:

post正常,一直无法通过validate_on_submit(),却没有error

忘记填写crsf_token 或crsf token填写错误

mark

报错keyerror: 'sqlalchemy_track_modifications'

运气不错,看了几个什么回退版本之类。从17年就出现的问题不应该18年了还在的bug啊。
果然找到完美的解决方案。

当我们的应用中有不止一个app时会出现这种错误。全局保留一个app,其他的impor
已有的app

view中import,而不是新实例化

from models import app

mark

将成功的喜悦flash到登录页面

确保注册的账号的唯一性。

from models import User    # 自定义字段验证规则: validate 下划线 字段名
    def validate_account(self, field):
        account = field.data
        user = User.query.filter_by(account=account).count()        if user > 0:            raise ValidationError(u"账号已存在")

实现验证码功能

定义一个验证码处理的文件:

安装pillow库

# encoding: utf-8import osimport uuid
__author__ = 'mtianyan'__date__ = '2018/2/17 0017 02:10'import randomfrom PIL import Image, ImageDraw, ImageFont, ImageFilterclass Captcha:
    """
    验证码功能类
    """
    # 随机一个字母或者数字

    def random_char(self):
        num = random.randint(1, 3)        if num == 1:            # 随机一个0-9之间数字: ascii码
            char = random.randint(48, 57)        elif num == 2:            # 随机一个a-z之间字母
            char = random.randint(97, 122)        else:            # 随机一个A-Z之间字母
            char = random.randint(65, 90)        # chr将数字转换为对应ASCII码数字字母
        return chr(char)    # 随机一个干扰字符
    def random_diss(self):
        arr = ["^", "_", "-", ".", "~"]        return arr[random.randint(0, len(arr) - 1)]    # 定义干扰字符颜色,干扰字符与字符颜色在不同区间
    def random_char_color(self):
        return (
            random.randint(                65, 255), random.randint(                65, 255), random.randint(                65, 255))    # 定义字符颜色, 三原色 RGB
    def random_diss_color(self):
        return (
            random.randint(                32, 127), random.randint(                32, 127), random.randint(                32, 127))    # 生成验证码:
    def create_captcha(self):
        width = 240  # 240px
        height = 60  # 60px

        # 创建一个图片
        image = Image.new("RGB", (width, height), (192, 192, 192))        # 创建font对象,定义字体和大小
        font_name = random.randint(1, 3)
        font_file = os.path.join(
            os.path.dirname(__file__),            "static/fonts") + "/%d.ttf" % font_name
        font = ImageFont.truetype(font_file, 40)        # 创建draw画布使图片可编辑,填充像素点
        draw = ImageDraw.Draw(image)        for x in range(0, width, 5):            for y in range(0, height, 5):
                draw.point((x, y), fill=self.random_diss_color())        # 填充干扰字符
        for v in range(0, width, 30):
            dis = self.random_diss()
            w = 5 + v            # 距离图片上边距最多15个像素, 最低五个像素
            h = random.randint(5, 15)
            draw.text((w, h), dis, font=font, fill=self.random_diss_color())        # 填充字符
        chars = ""
        for v in range(4):
            c = self.random_char()
            chars += str(c)            # 距离图片上边距最多15个像素, 最低五个像素
            h = random.randint(5, 15)            # 占图片宽度1/4, 10px间距, 顺序平移
            w = width / 4 * v + 10
            draw.text((w, h), c, font=font, fill=self.random_char_color())        # 模糊效果:
        image.filter(ImageFilter.BLUR)
        image_name = "%s.jpg" % uuid.uuid4().hex
        save_dir = os.path.join(
            os.path.dirname(__file__),            "static/captcha")        if not os.path.exists(save_dir):
            os.makedirs(save_dir)
        image.save(save_dir + '/' + image_name, "jpeg")        return dict(
            image_name=image_name,
            captcha=chars
        )
        image.show()if __name__ == "__main__":
    c = Captcha()
    print(c.random_char())
    print(c.random_diss())
    print(c.random_char_color())
    print(c.random_diss_color())
    c.create_captcha()

验证码有了我们该如何调用呢

引入

from flask import session, Response# 验证码@app.route("/captcha/", methods=["GET"])def captcha():
    from captcha import Captcha
    c = Captcha()
    info = c.create_captcha()
    image = os.path.join(os.path.dirname(__file__), "static/captcha") + "/" + info["image_name"]    with open(image, 'rb') as f:
        image = f.read()    return Response(image, mimetype="jpeg")

一个用于会话一个用于响应。

flask自带code,与我们命名的code会冲突,还好我一直命名captcha

报错:

UnicodeDecodeError: 'gbk' codec can't decode byte 0xff in position 0: illegal multibyte sequence

解决方案: 打开图片应该以二进制方式打开。

验证码实现显示:

mark

点击图片实现验证码刷新

<img  title="点击切换" src="/captcha/" ="this.src='/captcha/?'+Math.random()" style="width: 180px; height: 50px;margin-top: 6px;">
  1. 进行会话session中值的保存;

    session['captcha'] = info["captcha"]    print(session['captcha'])
  1. form中自定义验证码验证功能:

     # 自定义验证码规则: validate 下划线 字段名
    def validate_captcha(self, field):
        captcha = field.data        if not form_session["captcha"]:            raise ValidationError(u"非法操作")        if form_session["captcha"].lower() != captcha.lower():            raise ValidationError(u"验证码不正确")

实现用户登录功能

 form = LoginForm()    if form.validate_on_submit():
        data = form.data
        session["account"] = data["account"]
        flash("登录成功", "ok")        return redirect("/art/list/")

models中编写验证的方法。

    # 检查密码是否正确
    def check_pwd(self, pwd):        return check_password_hash(self.pwd, pwd)

forms中重写

    def validate_pwd(self, field):
        pwd = field.data
        user = User.query.filter_by(name=self.account.data).first()        if not user.check_pwd(pwd):            raise ValidationError(u"密码不正确")

login页面中,添加method 等于post

submit之前添加csrf token

添加账号,密码的报错信息。

mark

实现用户退出功能

    # 调用session的pop功能将user变为None
    session.pop("user", None)

登录成功后会跳转列表页面。但是列表页面的用户并没有动态显示。

如果出现编码问题

import sys   #引用sys模块进来,并不是进行sys的第一次加载  reload(sys)  #重新加载sys  sys.setdefaultencoding('utf8')  ##调用setdefaultencoding函数

将登陆成功中的闪现传递到art list中

mark

刷新等操作闪现就会消失

mark

用户登录权限控制(使用装饰器进行权限控制)

限制用户在未登录状态不能访问list等需要登录的页面。

# 登录装饰器def user_login_req(f):
    @wraps(f)    def login_req(*args,**kwargs):        if "user" not in session:
            return redirect(url_for('login', next=request.url ))        return f(*args,**kwargs)    return login_req

该装饰器传入一个函数,判断包装过后的函数对象。

为我们的退出,发布文章,编辑文章,删除文章,文章列表加上装饰器

@user_login_req

实现添加文章功能

views中添加文章。

def art_add():
    form = ArticleAddForm()    if form.validate_on_submit():
        data = form.data

forms中进行字段的验证

        validators=[
            DataRequired(u"内容不能为空")
        ],

照猫画虎,自行完成环节。

添加错误信息三部曲:

<form method="post" ><form method="post" enctype="multipart/form-data">

支持文件上传功能。

mark

照猫画虎为各个字段添加错误信息提示。

第三步:加上csrf令牌

mark

定义数据的保存以及图片的上传

from werkzeug.utils import secure_filename# 设置上传封面图路径app.config["uploads"] = os.path.join(os.path.dirname(__file__), "static/uploads")# 修改文件名称def change_name(name):
    info = os.path.splitext(name)    # 文件名: 时间格式字符串+唯一字符串+后缀名
    name = datetime.now().strftime("%Y%m%d%H%M%S")+str(uuid.uuid4().hex)+info[-1],    return name

握了根草,不小心多打了一个逗号。竟然不报错。而是报错我猜是它把info加上逗号当成了一个单元素tuple

builtins.TypeError
TypeError: must be str, not tuple

如果你跟着老师数据库都定义错了,这时候图片字段会保存的不是我们预期的值

mark

alter table article modify logo varchar(100) not null;

将model中的同步改为

logo = db.Column(db.String(100), nullable=False)  # 封面
def art_add():
    form = ArticleAddForm()    if form.validate_on_submit():
        data = form.data        # 上传logo
        file = secure_filename(form.logo.data.filename)
        logo = change_name(file)        if not os.path.exists(app.config["uploads"]):
            os.makedirs(app.config["uploads"])        # 保存文件
        form.logo.data.save(app.config["uploads"] + "/" + logo)        # 获取用户ID
        user = User.query.filter_by(account=session["user"]).first()
        user_id = user.id        # 保存数据,Article
        article = Article(
            title=data["title"],
            category=data["category"],
            user_id=user_id,
            logo=logo,
            content=data["content"],
            add_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        )
        db.session.add(article)
        db.session.commit()
        flash(u"发布文章成功", "ok")    return render_template("art_add.html", title="发布文章", form=form)

发布文章。多发布几篇测试数据

实现文章列表功能

# art_list 文章列表@app.route("/art/list/<int:page>/", methods=["GET"])  # 文章列表@user_login_reqdef art_list(page):    if page is None:
        page = 1
    # 只展示当前用户才能看到的内容
    user = User.query.filter_by(account=session["user"]).first()
    user_id = user.id
    page_data = Article.query.filter_by(
        user_id=user_id
    ).order_by(
        Article.add_time.desc()
    ).paginate(page=page, per_page=1)    return render_template("art_list.html", title="文章列表", page_data=page_data)

文章列表页取出当前用户的文章,以时间排序。将分页后的数据返回到前端html

前端html中展示数据

mark

此时访问art/list出错404.因为我们已经必须要接受参数了。、

所以在art_menu中设置默认第一页

mark

登录的时候,让它默认跳到第一页。

return redirect("/art/list/1/")

此时登录可以看到我们的列表页已经显示了出来。

mark

分类实现

 category = [(1, u"科技"), (2, u"搞笑"), (3, u"军事")]    return render_template("art_list.html", title="文章列表", page_data=page_data, category=category)

mark

使用数据库中保存的category数字在categorylist中取到对应的索引位置的元组

再通过索引1取到汉字

图片上传的展示

mark

分页功能

创建一个专门的分页的html。page.html

将原本list中用于分页的部分剪切到新的

mark

mark

如何调用这个分页page。

mark

mark

传入我们的数据和视图

实现编辑文章功能

文章列表中点击编辑文章,跳转到编辑文章

  1. 设计文章编辑的表单

mark

比文章添加只多了一个字段id

jinja2.exceptions.UndefinedError
jinja2.exceptions.UndefinedError: 'form' is undefined

因为我们只定义了form没有在edit视图中实例化,查询出数据

def art_edit(id):
    form = ArticleEditForm()
    article = Article.query.get_or_404(int(id))    return render_template("art_edit.html", form=form, title="编辑文章", article=article)

此时进行html中填充值

mark

mark

对于内容这里取值无效。所以我们在后端直接给form动态赋值。

    form.content.data = article.content
    form.category.data = article.category
    form.logo.data = article.logo

图片显示

mark

完整代码;

def art_edit(id):
    form = ArticleEditForm()
    article = Article.query.get_or_404(int(id))    if request.method == "GET":
        form.content.data = article.content
        form.category.data = article.category    # 莫名其妙赋初值:不赋初值表单提交时会提示封面为空
    # 放在这里修复显示请选择封面的错误
    form.logo.data = article.logo    if form.validate_on_submit():
        data = form.data        # 上传logo
        file = secure_filename(form.logo.data.filename)
        logo = change_name(file)        if not os.path.exists(app.config["uploads"]):
            os.makedirs(app.config["uploads"])        # 保存文件
        form.logo.data.save(app.config["uploads"] + "/" + logo)
        article.logo = logo
        article.title = data['title']
        article.content = data['content']
        article.category = data['category']
        db.session.add(article)
        db.session.commit()
        flash(u"编辑文章成功", "ok")    return render_template("art_edit.html", form=form, title="编辑文章", article=article)

其中注意的几个点。get请求时才赋初值,无论get post 都为logo赋初值。

实现删除文章功能

在art list中添加删除的链接。、

mark

视图中查询到对应的文章并删除。

# art_del 删除文章@app.route("/art/del/<int:id>/", methods=["GET"])  # 删除文章@user_login_reqdef art_del(id):
    article = Article.query.get_or_404(int(id))
    db.session.delete(article)
    db.session.commit()
    flash("删除《%s》成功!" % article.title, "ok" )    return redirect("/art/list/1")

本章总结

  1. bootstrap语法

  2. flask视图 路由 模板 静态文件创建

  3. jinja2模板语法

  4. 使用sqlachemy操作msyql

  5. 使用wtforms定义表单

      


点击查看更多内容
0人点赞

若觉得本文不错,就分享一下吧!

评论

相关文章推荐

正在加载中
意见反馈 去赚学费 帮助中心 APP下载
官方微信

举报

0/150
提交
取消