flask插件系列之Flask-WTF表单

flask_wtf是flask框架的表单验证模块,可以很方便生成表单,也可以当做json数据交互的验证工具,支持热插拔。

安装

pip install Flask-WTF

Flask-WTF其实是对wtforms组件的封装,使其支持对flask框架的热插拔。

简单使用

# app.py
from flask import Flask, current_app, request, render_template
from forms import MyForm

app = Flask(__name__,template_folder='static/html')
@app.route('/',methods=['GET','POST'])
def login():
    form = MyForm()
    if form.validate_on_submit():
        return 'OK'
    return render_template('forms/index.html', form=form)
if __name__ == '__main__':
    app.run(host='127.0.0.1', port=80, debug=True)

# forms.py
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired

class MyForm(FlaskForm):
    name = StringField('name', validators=[DataRequired()])

# forms/index.html
<form method="POST" action="/">
{{ form.csrf_token }}
{{ form.name.label }} {{ form.name(size=20) }}
<input type="submit" value="Go">
</form>

flask_wtf定义字段

flask_wtf完全使用wtforms组件的字段模型,wtforms对字段的定义在fields模块;又分为core和simple,core模块定义了普通使用的字段,simple在core模块的基础上扩展了一些字段,这些字段会自动进行字段级别的校验。

  • 字段类型
# core.py
__all__ = (
    'BooleanField', 'DecimalField', 'DateField', 'DateTimeField', 'FieldList',
    'FloatField', 'FormField', 'IntegerField', 'RadioField', 'SelectField',
    'SelectMultipleField', 'StringField',
)
常用字段说明:
BooleanField:布尔类型,如Flask,True
StringField:字符串类型
DecimalField:小数点文本字段,如:‘1.23’
DateField:日期字段,格式:'%Y-%m-%d'
DateTimeField:日期字段,格式:'%Y-%m-%d %H:%M:%S'
FieldList:统一字段类型组成列表,如:FieldList(StringField('Name', [validators.required()]))
FloatField:浮点数类型
IntegerField:整形
SelectMultipleField:多选框
RadioField:单选框

# simple.py
TextAreaField:文本域,可接受多行输入
PasswordField:密码字段,输入的不会直接在浏览器明文显示
FileField:上传文件,但不会处理验证文件,需要手动处理
HiddenField:隐藏字段
SubmitField:按钮
TextField:字符串类型的别名,弃用
  • 表单定义
# 参数:
class UserAdminForm(FlaskForm):
    username = StringField(label='用户名', validators=[DataRequired(),Length(4,20)])
    password_hash = PasswordField(label='密码',validators=[DataRequired(),Length(4,20)])
    limit = SelectField(label='用户权限',
                        choices=[('guest', '所有权限'),
                                 ('operation', '可读可写不可删除'),
                                 ('management', '可读不可写')],
                        default='guest')  # 权限

# 字段一般的参数
# label:字段的名字
# default:默认
# validators:字段的验证序列
# description:字段的描述
# choices:SelectField和他的子类有的字段,选择框,多选一
  • 字段的验证序列

字段的参数validators可以指定提交表单的验证序列,按照从左到右的顺序,默认的可选验证在wtforms.validators模块,已经封装的验证方法有:

__all__ = (
    'DataRequired', 'data_required', 'Email', 'email', 'EqualTo', 'equal_to',
    'IPAddress', 'ip_address', 'InputRequired', 'input_required', 'Length',
    'length', 'NumberRange', 'number_range', 'Optional', 'optional',
    'Required', 'required', 'Regexp', 'regexp', 'URL', 'url', 'AnyOf',
    'any_of', 'NoneOf', 'none_of', 'MacAddress', 'mac_address', 'UUID'
)
模块中大小写有对应的方式,如DataRequired对应data_required。

DataRequired/data_required:验证数据是否真实存在,即不能为空,必须是非空白字符串,否则触发StopValidation错误。
InputRequired/input_required:和DataRequired的区别在于可以是空白字符串;
Required/required:data_required的别名
Email/email:验证符合邮件的格式,只有最基本的验证;
EqualTo/equal_to:比较两个字段的值,比如密码和确认密码,如果不相等就触发错误,equal_to(field,message),需要输入另一个字段的名字。
IPAddress/ip_address:验证是否是ip地址,默认验证IPV4地址。
MacAddress/mac_address:验证是否符合mac格式;
UUID:是否是uuid格式;
URL/url:验证是否符合url格式;
Regexp/regexp:用提供的正则表达式验证字段;Regexp(r"")
Length/length:设置字段值的长度,Length(min,max);
NumberRange/number_range:设置一个数字字段的取值范围,可以针对浮点数和小数;NumberRange(min,max)
Optional/optional:允许字段为空并停止验证;
NoneOf/none_of:将传入的数据和无效的比较,是抛出异常;Noneof(values).
Anyof/any_of:将传入的数据和预设的数据比较,不是异常。Anyof(values).
  • 自定义字段验证

如果默认的验证序列不满足我们的要求,我们可以通过继承的方式自定义字段。

from wtforms.validators import DataRequired,Length,StopValidation
class NewStringField(StringField):
    """
    自定义一个新的字段
    """
    def pre_validate(self, form):
        """验证方法,在validators验证序列之前"""
        x:str = form.name.data
        if not x.startswith('g'):
            raise StopValidation("your data must be startswith 'g'")

    def post_validate(self, form, validation_stopped):
        """
        验证方法,在validators验证序列之后
        :param form:该字段所属的表单对象
        :param validation_stopped:前面验证序列的结果,True表示验证通过,False表示验证失败
        :return:
        """
        if not validation_stopped:
            raise ValueError("验证失败了!")
        pass
  • 触发StopValidation异常会停止验证链;

  • 自定义表单验证

一般来说,如果对表单有额外需要的验证,一般自定义表单的额外的验证方法而不是重新自定义新的字段,而form已经为我们提供了这种方法。
看Form对象的源码:

def validate(self):
    """
    Validates the form by calling `validate` on each field, passing any
    extra `Form.validate_<fieldname>` validators to the field validator.
    """
    extra = {}
    for name in self._fields:
        inline = getattr(self.__class__, 'validate_%s' % name, None)
        if inline is not None:
            extra[name] = [inline]

    return super(Form, self).validate(extra)

Form对象调用validate函数时会自动寻找validate_%s的方法添加到验证序列,并在原先字段的验证序列验证完毕后执行。

class MyForm(FlaskForm):
    name = StringField('name', validators=[DataRequired(), Length(4,20)])
    def validate_name(self, field):
        print(field.data)
        if hasattr(self, 'name') and len(self.name.data) > 5:
            print(self.name.data)
            return True
        raise ValidationError('超过5个字符')

# 在自定义的验证方法中,抛出异常使用ValidationError,validate会自动捕捉。

表单对象

flask_wtf推荐使用Form对象的子类FlaskForm代替,该对象提供了所有表单需要的属性和方法。那么Form对象是如何自动实现表单功能的呢?
分析FlaskForm对象源码:

class FlaskForm(Form):
    class Meta(DefaultMeta):
        def wrap_formdata(self, form, formdata):
            pass

    def __init__(self, formdata=_Auto, **kwargs):
        csrf_enabled = kwargs.pop('csrf_enabled', None)
        pass
    def is_submitted(self):
        pass
    def validate_on_submit(self):
        pass
    def hidden_tag(self, *fields):
        pass
    def validate(self):
        pass
  • FlaskForm内部定义了一个Meta类,该类添加csrf保护的一些方法,所以创建表单的时候一定要导入FlaskForm而不是Form.

  • is_submitted:检查是否有一个活跃的request请求;

  • validate_on_submit:调用is_submitted和validate方法,返回一个布尔值,用来判断表单是否被提交;

  • validate:字段级别的验证,每个字段都有一个validate方法,FlaskForm调用validate会对所有的字段调用validate方法验证,如果所有的验证都通过返回Ture,否则抛出异常。

  • hidden_tag:获取表单隐藏的字段;

  • wrap_formdata:获取request中的form,每次form对象初始化时会执行该函数从request获取form。

重要属性

form.data:字段名字和值组成的字典;
form.errors:验证失败的信息字典,在调用validate_on_submit方法后才有效;
form.name.data:字段name的值;
form.name.type:字段name的类型

常用场景

  • 登录验证
# froms.py
class UserPasswordForm(FlaskForm):
    """
    登录提交的表单
    """
    username = StringField('User', validators=[DataRequired()])
    password = PasswordField('Password', validators=[DataRequired()])

# form.html
<form method="POST" action="/">
{{ form.csrf_token }}
{{ form.username.label }} {{ form.username(size=20) }}
{{ form.password.label }} {{ form.password }}
<input type="submit" value="Go">
</form>

# views.py
@app.route('/login',methods=['GET','POST'])
def login():
    form = UserPasswordForm()
    if form.validate_on_submit():
        # 验证表单
        if form.username.data == "xiaoming" and form.password.data == '123':
            return 'OK'
    return render_template('forms/index.html', form=form)
  • ajax请求转化表单

有时候我们没有html页面的表单,只有ajax请求的数据交互,但是想借用Form来定义接口和验证接收的数据,如果ajax的请求方法是(‘POST’, ‘PUT’, ‘PATCH’, ‘DELETE’)中的一种,FlaskForm会自动从request对象中调用request.form和request.get_json()方法来接收数据,因此这种方式十分方便。注意:get方法不再其中。

# form.py
class MyForm(FlaskForm):
    name = StringField('name', validators=[DataRequired(), Length(4,20)])
# view.py
@app.route('/form',methods=['GET','POST'])
def form():
    if request.method != "GET":
        form = MyForm() # form会获取请求数据
        print(form.data)
        return 'ok'
    return ''
# test.py
import requests as req
import json

class ProTest():
    baseurl = 'http://127.0.0.1:80'
    def test_form(self):
        url = self.baseurl + '/form'
        rsp = req.post(url,json={'name':'hhhh'})
        # rsp = req.get(url,json={'name':'hhhh'})
if __name__ == '__main__':
    test = ProTest()
    test.test_form()
  • form启用csrf保护

默认csrf保护是开启的,只要在html文件中添加{{ form.csrf_token }},app必须设置SECRET_KEY参数。

# 禁用保护
form = Form(csrf_enabled=False)
# 或配置app时
WTF_CSRF_ENABLED = False
  • 一般数据csrf保护

同理必须设置SECRET_KEY参数。

from flask_wtf.csrf import CsrfProtect
csrf = CsrfProtect()

def create_app():
    app = Flask(__name__)
    csrf.init_app(app)

# 模板中添加一个隐藏域
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<meta name="csrf-token" content="{{ csrf_token() }}">
# 如果是ajax请求,可以在脚本中
var csrftoken = "{{ csrf_token() }}"
# 然后每个请求添加 X-CSRFToken 头部

# 全局禁用csrf
WTF_CSRF_CHECK_DEFAULT = False

# 对一些视图跳过csrf检查
@csrf.exempt
@app.route('/foo', methods=('GET', 'POST'))
def my_handler():
    return 'ok'

flask 源码之旅(基础)—import与模块

初识import

正所谓一方有难,八方点赞。君子生非异也,善假于物也---词子曰

讲个故事:

我们的小明同学,有天他发现自己需要一辆车。然后,他收拾行囊出发去云南西双版纳挖橡胶,准备先造个轮胎。这是C语言工程师.....

再讲个故事:

我们的小码同学,有天他发现自己需要一辆车。然后,他就去4S店提了一辆车。这就是python工程师.....

听完上面的故事,人民群众纷纷表示:

未分类

哈哈,当然有关系了,python之所以强大,就是因为它提供了丰富的各类模块,避免我们重复的造轮子。让我们花更少的时间,做更多的事情。大家要始终谨记信条:“人生苦短,我用python”。

flask作为一个优秀的python web框架,正是如此在践行我们的信条,打开flask的源码,排在前面的永远是各种import。

作为一名python小白,面对这一坨import,面对苍天,涕然泪下:“这里面一定隐藏了肮脏的py交易,不然纯洁的我怎么会看不透这一切”。

未分类

没错是这个样子的,这里面的确隐藏了一堆肮脏的py交易。正是通过这一坨坨import,flask才变得如此强大,下面就让本人,一名普通的python工作者,来为你解开黑幕。

模块

莎士比亚曾经说过:

一千个观众眼中有一千个哈姆雷特,一个python程序员就可以导入一千个模块

模块是什么?

A module is a file containing Python definitions and statements.

模块就是一个普通的.py结尾的文件,里面包含了你写的python语句,定义的python变量。

模块之于一个完整的python程序,就像车轮之于车,装逼之于我。

未分类

为什么一定要有模块?

小明有一个G的种子,小码作为他的好兄弟难道想看片还要去自己再找一遍吗?现在这世道,天道苍凉,世风日下,快播都被封了,多难找片啊!小明他忍心对这一切熟视无睹吗?

不…..小明不忍心,他会拍拍小码的肩膀,给他一个会意的眼神:“拿去用,U盘自己来拷”。

为什么要有模块? 正是因为这个世界存在了太多没有必要的重复性的劳动。

怎么使用模块?

思考这个问题之前,我们先思考一下,小码应该怎么给小明要片看?

未分类

两种方式:

  1. 小码可以把小明所有的珍藏都拿过来,想看那个看那个。
  2. 小码可以直接从小明那里拿到想看的片子。

现在我们再回过头看一下python,python提供了import关键字,专门用于让你要片的……啊呸,是导入模块的。

首先,你可以这么导:

import A

没错,简单粗暴,想用到那个模块的内容,就直接把整个模块导入进来。

譬如你想使用模块A里的函数b,这个时候就可以:

A.b()

再然后,你还可以这么导:

from A import b

想用到模块里的什么具体内容,就从模块中把具体的内容导入即可。

b()

想看武藤兰,就直接要武藤兰,想看苍井空,就明白的要苍井空。如果你既想要苍井空,又想要武藤兰:

from A import a, b

未分类

现在问题又来了,小码的电脑里已经有了一部苍老师了,这是从小刚那边又拷贝进来一部苍老师,文件名冲突了怎么办?

会用电脑的都知道,右键重命名文件,避免文件名冲突不就行了~

python 在导入模块时也提供了重命名的机制,它也要是为了避免命名冲突的。

于是你可以:

import A as B

还可以:

from A import a as b

import做了什么?

上面我们已经了解到了,import的作用就是导入模块。那么它具体做了什么?

和这个问题有异曲同工之妙的问题就是:怎么把大象放进冰箱里?

未分类

把大象放进冰箱里:

第一步,打开冰箱门。
第二步,把大象放进去。

把模块导入进来:

第一步,找到模块。
第二步,把模块放进来。

搜索模块路径

想要找到模块小可爱,第一个要去找的地方就是内建模块。

内建模块,顾名思义就是python语言的内置模块,典型的如os模块,sys模块。

内建模块在linux系统中,通常存放于/usr/lib64/python2.7/。

内建模块里没有的找到,就会去sys.path中找,sys.path再找不到,那就是真的找不到了。

有些会python的童鞋忍住不要问了,在内建模块里没找到,不应该去python进程执行路径或者环境变量$PYTHONPATH中搜寻吗?怎么到你这,就成了去sys.path中搜寻了?

其实在解释器启动时,它会把上面的两个路径加入到sys.path中的。

当前解释器被启动的路径:/root

未分类

将解释器启动路径切换到:/root/test

未分类

再设置一下环境变量:export PYTHONPATH=$PYTHONPATH:/root/test2

未分类

python解释器在启动时,除了会把python进程执行路径或者环境变量$PYTHONPATH导入到sys.path中外,还会依照一些规则导入一些其他路径到sys.path中。

所以总结一下:

如果你想导入一个模块,却告诉你ImportError找不到模块。这个时候你就要打印一下你的sys.path,检视一下模块的路径是否在你的sys.path中。

把模块放进来

前面有提到过,什么是模块?模块说白了也是一个普通的py文件而已。

顺便提一下,py文件只是一个普通的文本文件而已,不知道什么是文本文件的童鞋,你现在看到的就是文本文件。

python解释器想真正执行起来py文件,需要把这个py文件转换为字节码。什么是字节码?就是python解释器能够看懂进而按照它去执行的东西。就像老外对你说英语,你大脑里会有一个把英文转换为中文释义的过程,然后你才能明白老外是在讲嘛~

import导入模块时,它会找到这个模块的代码文本,把它转换成字节码,然后从头到位执行一遍这个模块。

我们可以自行测试一下:

1.建一个test.py文件,在里面加入以下代码:

print "hehe"

2.打开python解释器,导入test模块,我们可以看到”hehe”被执行输出了

未分类

import导入模块的本质,就是把整个模块执行一遍,生成一个python模块对象。import test则是用test这个变量去引用模块对象。(ps:此处看不懂无所谓,后面会讲到的)。

另外,需要注意的是,import导入模块对象,只会在第一次import才会真正执行上面讲的导入流程,重复import并不会重复导入模块。

未分类

可以看到”hehe”只被输出了一次。

原因是在执行import后,python会把导入的模块缓存在sys.modules中。

未分类

sys.modules中已经存在的模块,不会再执行模块导入流程。

聪明的童鞋,应该就可以想到在搜索模块路径小章节,我们有提到import导入模块时,首先会查看是否是内建模块。

未分类

其实那个描述也是不太准确的,解释器在启动时就会把内建模块都加载到sys.modules中。与其说查看被导入的模块是否是内建模块,不如说查看被导入的模块是否在sys.modules中更准确。

尾声

文章到了这里,暂时就可以告一段落了。有不明白的问题或者文章以外不明白的问题欢迎留言探讨。

Flask学习11:阿里云新手Flask + nginx + uwsgi + ubuntu的完整项目部署教程

Flask项目部署

web工作原理

客户端 < = > 服务器(nginx) < = > uWSGI < = > Python(Flask) < = > 数据库

nginx安装

源码安装、apt-get install …

sudo /usr/local/nginx/sbin/nginx -s reload

添加虚拟主机

1.在nginx的主配置文件最后一个大括号的上面添加:

include vhost/*.conf;

2.在conf/下新建文件夹,用于保存所有的虚拟主机配置文件

mkdir vhost

3.vhost目录下新建一个虚拟主机的配置文件(sudo vim idandan.vip.conf)

server{
    listen 80;
  server_name idandan.vip;
  location / {
      root html/blog;
      index index.html;
  }
}

# sudo vim idandan.vip.conf

4.在html目录下创建blog文件夹,在blog下新建index.html

Hello World

5.重启nginx(sudo /usr/local/nginx/sbin/nginx -s reload)

6.添加本地的域名解析,修改文件:(C:WindowsSystem32driversetchosts)

最后一行添加:139.196.81.238 idandan.vip

7.测试,在浏览器地址栏输入:idandan.vip

uWSGI

1.安装:pip3 install uwsgi

2.配置:

http:    # 采用http协议
socket:      # 采用socket协议
wsgi-file    # 将数据交给哪个模块
callable # 具体可调用的对象
chdir        # 指定uwsgi启动后的当前目录
daemonize    # 后台运行,需要指定一个日志文件


# blog.py文件

from flask import Flask
app = Flask(__name__)

@app.route('/')
def index():
 return 'Hi'

if __name__ == '__main__':
 app.run()

3.简单实例

1.在blog目录下创建blog.py文件,里面写上flask启动代码用于测试
2.启动:
    sudo uwsgi --http host:port --wsgi-file blog.py --callable app

4.socket方式启动

1)nginx转发请求:

python 
server{ 
listen 80; 
server_name idandan.vip; 
location / { 
# root html/blog; 
# index index.html; 
include uwsgi_params; 
uwsgi_pass 127.0.0.1:5000; 
} 
} 

2)以socket方式启动

sudo uwsgi --socket 127.0.0.1:5000 --wsgi-file blog.py --callable app

3)将启动参数配置写进配置文件

[uwsgi]
socket  = 127.0.0.1:5000
wsgi-file = blog.py
callable = app

(blog/vim uwsgi.ini)
启动:sudo uwsgi wsgi.ini

静态文件处理

1.准备静态资源

python
1.在项目根目录下(blog)创建static目录
2.将图片拷贝到static下

2.配置nginx转发

# 添加一个location

location /static{
 # root html/blog;

# 或

alias html/blog/static;  # 两种方式都可以
}

Flask学习10:Flask项目集成富文本编辑器CKEditor 上传图片

CKEditor下载地址:https://ckeditor.com/ckeditor-4/download/
访问CKeditor官方网站,进入下载页面,选择Standard Package(一般情况下功能足够用了),如果你想尝试更多的功能,可以选择下载Full Package。
下载好CKeditor之后,解压到Flask项目static/ckeditor目录即可。

在Flask项目中集成CKEditor:

  1. <scripts>标签中导入CKEditor
  2. 使用CKEDITOR.replace()把现存的<textarea>替换成CKEditor

代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

{#    引入CKEditor#}
    <script type="text/javascript" src="../static/js/ckeditor/ckeditor.js"></script>
</head>
<body>
    <form method="post" action="{{ url_for('ckupload') }}" enctype="multipart/form-data">
        <input type="text" name="subject" placeholder="主题">
        <textarea name="content" id="content" placeholder="内容"></textarea>
        <input type="submit" value="立即发布">

{#        替换textarea#}
        <script type="text/javascript">
            CKEDITOR.replace('content', {
{#        开启上传功能,配置上传路径#}
                filebrowserUploadUrl: '/ckupload/',
            });
        </script>
    </form>
</body>
</html>

Flask处理上传请求:

CKEditor上传功能是统一的接口,即一个接口可以处理图片上传、文件上传、Flash上传。先来看看代码:

from flask import Flask, render_template, request, make_response, url_for
from flask_script import Manager
import datetime
import os
import random

app = Flask(__name__)
manager = Manager(app)

def gen_rnd_filename():
    filename_prefix = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
    return '%s%s' % (filename_prefix, str(random.randrange(1000, 10000)))

@app.route('/ckupload/', methods=['POST'])
def ckupload():
    """CKEditor file upload"""
    error = ''
    url = ''
    callback = request.args.get("CKEditorFuncNum")
    if request.method == 'POST' and 'upload' in request.files:
        fileobj = request.files['upload']
        fname, fext = os.path.splitext(fileobj.filename)
        rnd_name = '%s%s' % (gen_rnd_filename(), fext)
        filepath = os.path.join(app.static_folder, 'upload', rnd_name)
        # 检查路径是否存在,不存在则创建
        dirname = os.path.dirname(filepath)
        if not os.path.exists(dirname):
            try:
                os.makedirs(dirname)
            except:
                error = 'ERROR_CREATE_DIR'
        elif not os.access(dirname, os.W_OK):
            error = 'ERROR_DIR_NOT_WRITEABLE'
        if not error:
            fileobj.save(filepath)
            url = url_for('static', filename='%s/%s' % ('upload', rnd_name))
    else:
        error = 'post error'
    res = """

<script type="text/javascript">
  window.parent.CKEDITOR.tools.callFunction(%s, '%s', '%s');
</script>

""" % (callback, url, error)
    response = make_response(res)
    response.headers["Content-Type"] = "text/html"
    return response

@app.route('/', methods=['POST', 'GET'])
def index():
    return render_template('index.html')

if __name__ == '__main__':
    manager.run()

再去看自己项目的目录,就可以找到/upload底下已经有自己上传的图片了。

原文:http://flask123.sinaapp.com/article/49/

Flask学习9:在服务器上处理富文本

继上一篇博客(Flask学习8),提交表单后,POST 请求只会发送纯 Markdown 文本,页面中显示的 HTML 预览会被丢掉。和表单一起发送生成的 HTML 预览有安全隐患,因为攻击者轻易就能修改 HTML 代码,让其和 Markdown 源不匹配,然后再提交表单。安全起见,只提交 Markdown 源文本,在服务器上使用 Markdown(使用 Python 编写的 Markdown 到 HTML 转换程序)将其转换成 HTML。得到 HTML 后,再使用 Bleach 进行清理,确保其中只包含几个允许使用的HTML 标签。

把 Markdown 格式的博客文章转换成 HTML 的过程可以在 _posts.html 模板中完成,但这么做效率不高,因为每次渲染页面时都要转换一次。为了避免重复工作,我们可在创建博客文章时做一次性转换。转换后的博客文章 HTML 代码缓存在 Post 模型的一个新字段中,在模板中可以直接调用。文章的 Markdown 源文本还要保存在数据库中,以防需要编辑,修改后的模型如下:

...
from markdown import markdown
import bleach

class Posts(db.Model):
    ...
    content_html = db.Column(db.Text)

    @staticmethod
    def on_changed_content(target, value, oldvalue, initiator):
        allowed_tags = [
                        'a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
                        'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
                        'h1', 'h2', 'h3', 'p'
                        ]
        target.content_html = bleach.linkify(bleach.clean(markdown(value, output_format='html'), tags=allowed_tags, strip=True))
db.event.listen(Posts.content, 'set', Posts.on_changed_content)

on_changed_body 函数注册在 body 字段上,是 SQLAlchemy“set”事件的监听程序,这意味着只要这个类实例的 body 字段设了新值,函数就会自动被调用。on_changed_body 函数把 body 字段中的文本渲染成 HTML 格式,结果保存在 body_html 中,自动且高效地完成Markdown 文本到 HTML 的转换。真正的转换过程分三步完成。首先,markdown() 函数初步把 Markdown 文本转换成 HTML。
然后,把得到的结果和允许使用的 HTML 标签列表传给 clean() 函数。clean() 函数删除所有不在白名单中的标签。转换的最后一步由 linkify() 函数完成,这个函数由 Bleach 提供,把纯文本中的 URL 转换成适当的链接。最后一步是很有必要的,因为 Markdown规范没有为自动生成链接提供官方支持。PageDown 以扩展的形式实现了这个功能,因此在服务器上要调用 linkify() 函数。

最后,如果 post.body_html 字段存在,还要把 post.body 换成 post.body_html:

# 文章内容部分显示
<a href="#">
    {% if post.content_html %}
          {{ post.content_html }}
   {% else %}
          {{ post.content }}
   {% endif %}
</a>

Flask学习8:使用markdown

对于发布短消息和状态更新来说,纯文本足够用了,但如果用户想发布长文章,就会觉得在格式上受到了限制,因此可以使用markdown。
依赖:

  • PageDown:使用 JavaScript 实现的客户端 Markdown 到 HTML 的转换程序。
  • Flask-PageDown:为 Flask 包装的 PageDown,把 PageDown 集成到 Flask-WTF 表单中。
  • Markdown:使用 Python 实现的服务器端 Markdown 到 HTML 的转换程序。
  • Bleach:使用 Python 实现的 HTML 清理器。
这些包都可以用pip安装: 
pip install flask-pagedown markdown bleach

Flask-PageDown 扩展定义了一个 PageDownField 类,这个类和 WTForms 中的 TextAreaField接口一致。使用 PageDownField 字段之前,先要初始化扩展,

...
from flask_pagedown import PageDown

# 创建对象
...
pagedown = PageDown()

# 初始化对象
def config_extensions(app):
    ...
    pagedown.init_app(app)

若想把首页中的多行文本控件转换成 Markdown 富文本编辑器,表单中的字段要进行修改,

# 发布文章表单
class PublishForm(FlaskForm):
    title = StringField('',render_kw={'placeholder':'主题'}, validators=[DataRequired()])
    # markdown
    content = PageDownField('',render_kw={'placeholder':'内容'}, validators=[DataRequired(), Length(1, 9999, message='文章字数超出限制')])

    submit = SubmitField('立即发布')

Markdown 预览使用 PageDown 库生成,因此要在模板中修改。Flask-PageDown 简化了这个过程,提供了一个模板宏,从 CDN 中加载所需文件:

{% extends 'base/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block title %}
    记录
{% endblock %}

{% block scripts %}
    {{ super() }}
{#    markdown#}
    {{ pagedown.include_pagedown() }}
{% endblock %}

{% block page_content %}
    <div style="width: 800px; height: 800px; background-color: white; margin: 0 auto">
        {{ wtf.quick_form(form) }}

    </div>
{% endblock %}

做了上述修改后,在多行文本字段中输入 Markdown 格式的文本会被立即渲染成 HTML 并显示在输入框下方。

未分类

Flask学习7:完整项目(blog)

Flask完整项目:Blog

# manage.py代码
import os
from flask_script import Manager
from flask_migrate import MigrateCommand
from app import create_app

app = create_app(os.environ.get('FLASK_CONFIG') or 'default')

manager = Manager(app)
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
    manager.run()

由于代码较多,可下载查看。
http://download.csdn.net/download/qq_25046261/10176467

Flask学习6:博客项目基本构思

Flask项目

项目需求

  1. 用户注册登陆
  2. 用户信息管理
  3. 博客发表、评论
  4. 博客展示(分页)
  5. 收藏(点赞)
  6. 搜索、统计、排序、…

项目结构

blog/                           # 项目根目录
    app/                        # 程序包目录
        static/                 # 静态文件目录
            js/                 # js文件目录
            css/                # css文件目录
            img/                # 图片文件目录
        templates/              # 模板文件目录
        views/                  # 视图函数(蓝本)
        models/                 # 所有的数据模型文件
        forms/                  # 所有的表单文件
        config.py               # 配置文件
        email.py                # 邮件发送
        extensions.py           # 所有扩展
        __init__.py             # 作为一个包必须有
    migrations/                 # 数据库迁移目录
    tests/                      # 测试文件目录
    venv/                       # 虚拟环境
    requirements.txt            # 项目依赖包列表我呢见
    manage.py                   # 启动控制文件

开发环境

1.新建一个项目,按照需求创建需要的目录及文件

2.创建虚拟环境

virtualenv venv  # 创建虚拟环境
venvScriptsactivate  # 启动虚拟环境
venvScriptsactivate  # 退出虚拟环境

3.依赖包管理

生成依赖环境:pip freeze > requirements.txt

下载依赖包:pip install -r requirements.txt

书写步骤

1.配置文件的书写与使用

1.在config.py文件中书写项目配置
2.在app/__init__.py中封装create_app函数
3.在manage.py文件中调用create_app函数并启动实例

2.添加各种扩展

1.在app/extensions.py中,创建扩展对象,封装初始化函数config_extensions
2.在create_app函数中调用配置函数即可

3.添加蓝本

1.在view目录下创建文件,在新建的文件中创建蓝本,添加视图函数等
2.在views目录下创建__init__.py文件中,封装一个config_blueprint函数,完成蓝本注册
3.为了简化蓝本注册,多写一个蓝本配置的元组,然后遍历执行注册
4.自行添加新的蓝本时,只需要导入,然后再配置中增加一项即可

4.项目基础模板定制

1.基础模板定制
2.为了测试,顺便定制了错误显示页面(config_errorhandler)

5.邮件的异步发送

1.http://blog.csdn.net/qq_25046261/article/details/78914370#t5异步发送邮件的两个函数
2.导入对应的依赖

Flask学习5:模型

数据模型

数据库回顾

1.分类

​关系型数据库:MySQL、Oracle、SQLite、…

非关系型数据库:MongoDB、Redis、…

2.选择

​数据库没有好坏,要根据项目需求进行选择:盲目的评价或跟风只能证明不够

flask-sqlalchemy

1.说明

​提供了大多数关系型数据库的支持,而且提供了ORM(对象关系映射)

2.安装

pip install flask-sqlalchemy

3.连接配置

​指定数据库地址

MySQL:mysql://username:password@host/database

SQLite:
        windows:sqlite:///c:/path/to/database
         linux:sqlite:////c:/path/to/database

配置选项:
        SQLACHEMY_DATABASE_URI

4.使用:

from flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy


app = Flask(__name__)
manager = Manager(app)

# 配置数据库连接地址
import os
# 当前路径
base_dir = os.path.abspath(os.path.dirname(__file__))
# 连接地址
database_uri = 'sqlite:///' + os.path.join(base_dir, 'data.sqlite')
app.config['SQLALCHEMY_DATABASE_URL'] = database_uri

# 创建对象
db = SQLAlchemy(app)

5.添加数据模型

# 继承自特定的基类
# 定义模型类
class User(db.Model):
    # 不指定表明,默认会将大驼峰转换为小写+下划线风格
    # 如:类名为UserModel =》user_model

    # 指定表名
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(32), unique=True)
    email = db.Column(db.String(64), unique=True)

6.创建表、删除表

# 创建表,若是sqlite没有数据库时会自动创建,若是MySQL会报错
@app.route('/')
def index():
    # 删除表
    db.drop_all()
    # 创建表
    db.create_all()

    return 'Nice day!'

7.添加命令行删除表和创建表

from flask_script import prompt_bool

# 创建表python model.py createall
@manager.command
def createall():
    db.drop_all()
    db.create_all()
    return '数据表已创建'

# 删除表python model.py dropall
@manager.command
def dropall():
    if prompt_bool('确定要删库跑路吗?'):
        db.drop_all()
        return '数据表已删除'
    return '还是在考虑一下吧'

创建表:python model.py createall
删除表:python model.py dropall

8.自定义终端shell命令

原因是:系统默认有一个shell命令,启动后可以进行终端测试,但是没有导入任何数据,因此需要自己定制。

# 定制shell
def shell_context():
    # 返回在shell环境中需要的数据,以字典的形式返回
    return dict(db=db, User=User)
manager.add_command('shell',Shell(make_context=shell_context))

数据的CURD操作

1.增加数据

# 在请求结束后,自动提交数据库操作(执行commit),否则每次都要手动commit
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

# 添加数据
@app.route('/insert/')
def insert():
    # 创建数据模型
    # zhangsan = User(username='zhangsan', password='12345', sex=False, email='[email protected]')
    # lisi = User(username='lisi', password='12345', sex=False, email='[email protected]')
    # mazi = User(username='mazi', password='12345', sex=False, email='[email protected]')
    xjt = User(username='xjt', password='12345', sex=False, email='[email protected]')

    # 添加到会话中
    db.session.add(xjt)
    # 添加多个
    # db.session.add_all([zhangsan, lisi, mazi])

    # 提交
    db.session.commit()  # 如果设置了SQLALCHEMY_COMMIT_ON_TEARDOWN就不用手动commit了
    return '已经添加'

2.查询数据

# 根据主键查询数据
@app.route('/select/<uid>')
def select(uid):
    # 根据主键进行查询,如果有,返回User对象,没有None
    u = User.query.get(uid)
    if u:
        return u.username
    return '查无此人'

3.更新数据

# 修改(更新)数据
@app.route('/update/<uid>')
def update(uid):
    u = User.query.get(uid)
    if u:
        u.username = u.username + 'Copy'
        # 更新没有单独的函数,当添加的对象有id时被认为时更新操作
        db.session.add(u)
        # db.session.commit()
        return '修改完成'
    return '查无此人'

4.删除数据

# 删除数据
@app.route('/delete/<uid>')
def delete(uid):
    u = User.query.get(uid)
    if u:
        db.session.delete(u)
        return '删除成功'
    return '查无此人'

真是的项目很少用到物理删除(彻底从磁盘删掉),大多数都是使用逻辑删除(打标记)

5.各种查询

# 各种查询
@app.route('/selectby/')
def select_by():
    # 根据主键进行查询
    # u = User.query.get(2)
    # return u.username

    # 查询所有满足条件的
    # users = User.query.all()
    # return ','.join([u.username for u in users])

    # 指定过滤条件(只能是等值条件)
    # u = User.query.filter_by(username='Dandan').first()
    # return u.username

    # 指定过滤条件(可以是等值条件)
    # u = User.query.filter(User.id > 2).first()
    # u = User.query.filter(User.id == 2).first()
    # return u.username

    # 有就返回,没有报404
    # u = User.query.get_or_404(8)
    # u = User.query.filter(User.id > 8).first_or_404()
    # return u.username

    # 数据统计
    total = User.query.count()
    return str(total)

自行测试:limit, offset, order_by, paginate

模型设计参考

1.常见的字段类型

未分类

2.常见选项

未分类

3.总结

1.插入数据时,可以不传值的情况:自增的主键、有默认值、可以为空
2.flask-sqlalchemy:要求每个模型都有一个主键,通常为id

数据库的迁移

1.说明

​当数据模型更改时,需要将更改应用到数据库,这个过程叫数据库迁移。

​直接删除,然后再创建太过于简单粗暴,副作用大(原来的数据全部丢失)

​更好的解决方案是:既能将修改应用到数据库,又不删除原来的数据,若自己不会,可以采用flask-migrate扩展库.

2.安装

pip install flask-migrate

3.配置

from flask_migrate import Migrate, MigrateCommand

# 创建对象
migrate = Migrate(app, db)

# 添加终端命令
manager.add_command('db', MigrateCommand)

4.使用

1.初始化数据库迁移的仓库,执行一次就行了
    python manage.py db init
2.创建迁移脚本,会根据数据模型与数据库的表的差异生成sql语句
    python manage.py db migrate
3.执行迁移,就是执行上面生成的sql语句
    python manage.py upgrade
4.以后再迁移,只需2、3两步即可

Flask学习4:文件上传与邮件发送

文件上传与邮件发送

原生上传文件

1.添加一个模板文件html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>文件上传</title>
</head>
<body>
    {% if img_url %}
        <img src="{{ img_url }}">
    {% endif %}
    <form method="post" enctype="multipart/form-data">
        <input type="file" name="photo">
        <input type="submit" value="上传">
    </form>
</body>
</html>

2.添加视图函数

@app.route('/upload/', methods=['GET', 'POST'])
def upload():
    img_url = None
    if request.method == 'POST':
        photo = request.files.get('photo')
        if photo and allowed_file(photo.filename):
            # 获取文件后缀
            suffix = os.path.splitext(photo.filename)[-1]

            # 随机生成文件名
            photoname = random_string() + suffix
            # 保存上传的文件
            photo.save(os.path.join(app.config['UPLOAD_FOLDER'], photoname))

            img_url = url_for('uploaded', filename=photoname)
    return render_template('upload.html', img_url = img_url)

3.相关配置及函数

# 允许上传的文件后缀
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

# 配置上传文件的保存位置
app.config['UPLOAD_FOLDER'] = os.getcwd()

# 限制上传文件大小
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 8

# 判断是否是允许的文件后缀
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

# 获取上传文件的url
@app.route('/uploaded/<filename>')
def uploaded(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

# 生成随机字符串
def random_string(length = 32):
    import random
    base_str = 'abcdefghijklmnopqrstuvwxyz1234567890'
    return ''.join([random.choice(base_str) for i in range(length)])

4.注意事项,文件上传失败时,应从哪些方面着手

1.表单的提交方法必须是POST
2.表单的enctype属性必须设置(multipart/form-data)
3.上传的字段类型必须为file, 并且必须有name属性
4.是否超过了允许的最大尺寸
5.文件的保存位置是否还有空间,是否有权限

5.生成缩略图(需要安装Pillow库)

# 保存上传的文件
photo.save(pathname)

# 生成缩略图
# 1.打开文件
img = Image.open(pathname)
# 2.重新设置尺寸
img.thumbnail((128, 128))  # 参数为元组,指定宽高
# 3.保存修改
img.save(pathname)

扩展

环境变量:好处是可以避免隐私信息公布于众(设置时注意等号两边不要加空格)。

windows配置

设置:set NAME=dandan
获取:set NAME

linux配置

导出:export NAME=dandan
获取:echo $NAME

代码获取

@app.route('/env/')
def env():
   return os.environ.get('NAME', 'Ahh')

flask-uploads

1.说明

​在文件上传时,提供了很大的方便,比如文件类型的过滤、校验等

2.安装

pip install flask-upload

3.使用

from flask_uploads import UploadSet, IMAGES, configure_uploads, patch_request_class


# 上传文件的大小

app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 8

# 上传文件的保存位置

import os
app.config['UPLOADED_PHOTOS_DEST'] = os.getcwd()


# 创建上传对象, 需要指定过滤的文件后缀

photos = UploadSet('photos', IMAGES)
configure_uploads(app, photos)


# 配置上传文件大小, 默认64M,设置为None时使用MAX_CONTENT_LENGTH的大小

patch_request_class(app, size=None)


@app.route('/upload/', methods=['GET', 'POST'])
def upload():
   img_url = None
   if request.method == 'POST' and 'photo' in request.files:
       # 保存文件
       filename = photos.save(request.files['photo'])

       # 获取保存文件的url
       img_url = photos.url(filename)
   return render_template('upload.html', img_url = img_url)





# html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>文件上传</title>
</head>
<body>
   {% if img_url %}
       <img src="{{ img_url }}">
   {% endif %}
   <form method="post" enctype="multipart/form-data">
       <input type="file" name="photo">
       <input type="submit" value="上传">
   </form>
</body>
</html>

完整的文件上传

1.flask-uploads配置同上

2.flask-wtf配置

# 导入表单基类

from flask_wtf import FlaskForm

# 导入文件上传字段及验证器

from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField


# 上传文件表单类

class UploadForm(FlaskForm):
   photo = FileField('头像上传', validators=[FileRequired('文件未选择'), FileAllowed(photos, message='文件类型错误')])
   submit = SubmitField('上传')

3.视图函数

# 随机生成文件名

def random_string(length = 32):
   import random
   base_str = '1234567890qwertyuiopasdfghjklmnbvcxz'
   return ''.join([random.choice(base_str) for i in range(length)])

@app.route('/upload/', methods=['GET', 'POST'])
def upload():
   img_url = None
   form = UploadForm()
   if form.validate_on_submit():
       # 获取文件后缀
       suffix = os.path.splitext(form.photo.data.filename)[1]
       filename = random_string() + suffix

       # 保存上传文件
       photos.save(form.photo.data, name=filename)

       # 生成缩略图
       pathname = os.path.join(app.config['UPLOADED_PHOTOS_DEST'], filename)
       # 打开文件
       img = Image.open(pathname)

       # 设置尺寸
       img.thumbnail((128, 128))

       img.save(pathname)

       img_url = photos.url(filename)
   return render_template('upload2.html', form=form, img_url=img_url)

4.模板文件

{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block content %}
   <div class="container">
       {% if img_url %}
           <img src="{{ img_url }}">
       {% endif %}

       {{ wtf.quick_form(form) }}
   </div>
{% endblock %}

flask-mail

1.说明

​是一个邮件发送的扩展库,使用非常方便

2.安装

pip install flask-mail

3.配置

from flask_mail import Mail, Message


# 配置一定要写在创建Mail对象之前,否则不起作用


# 邮箱服务器

import os
app.config['MAIL_SERVER'] = os.environ.get('MAIL_SERVER', 'smtp.163.com')

# 用户名

app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME', '[email protected]')

# 密码,密码有时是授权码

app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD', 'xxxxxx')


# 创建对象

mail = Mail(app)

4.发送邮件

@app.route('/')
def hello_world():
   # 创建邮件对象
   msg = Message(subject='丹丹的测试邮件', recipients=['[email protected]'], sender=app.config['MAIL_USERNAME'])
   # 浏览器打开显示这个
   msg.html = '<h1>Nice day</h1>'
   # 终端打开显示这个
   msg.body = 'Body'
   mail.send(msg)
   return '邮件已发送!'

5.封装函数,发送邮件

# 封装函数,发送邮件

def send_mail(to, subject, template, **kwargs):
   # 创建邮件对象
   msg = Message(subject=subject, recipients=to, sender=app.config['MAIL_USERNAME'])
   # 浏览器打开显示这个
   msg.html = '<b>Nice day</b>'
   # 终端打开显示这个
   msg.body = 'Nice day'
   mail.send(msg)

6.异步发送邮件

# 异步发送邮件

def async_send_mail(app, msg):
   # 发送邮件需要程序上下文,新的线程中没有上下文,需要手动创建
   with app.app_context():
       # 发送邮件
       mail.send(msg)



# 封装函数,发送邮件

def send_mail(to, subject, template, **kwargs):
   # 根据current_app获取当前实例
   app = current_app._get_current_object()
   # 创建邮件对象
   msg = Message(subject=subject, recipients=to, sender=app.config['MAIL_USERNAME'])
   # 浏览器打开显示这个
   msg.html = render_template(template + '.html', **kwargs)
   # 终端打开显示这个
   msg.body = render_template(template + '.txt', **kwargs)
   # mail.send(msg)

   # 创建线程
   thr = Thread(target=async_send_mail, args=[current_app, msg])
   # 启动线程
   thr.start()
   return thr