《Flask web 开发从入门到精通》读书笔记(上)

《Flask web 开发从入门到精通》

  • 前言
    • 官方文档
    • 代码资源
    • python -m参数
    • virtualenvwrapper
    • MVC
    • 随记
  • 第1章 配置Flask
    • 1.2 处理基本的配置问题
    • 1.4 组织静态文件
    • 1.5 实例文件夹
    • 1.6 视图和模型
  • 第2章 基于jinja2的模板机制
    • 2.1 引导推荐的布局
    • 2.2 实现块组合和继承布局
    • 2.3 自定义上下文处理器
    • 2.4 创建自定义 jinja2 过滤器
    • 2.5 为表单创建自定义宏
    • 2.6 高级日期和时间格式
  • 第3章 Flask中的数据模型
    • 3.1 创建SQLAchemy DB 实例
    • 3.2 创建基本的模型
    • 3.3 创建关系分类模型
    • 3.4 利用 Alembic 和 Flask-Migrate 迁移数据库
    • 3.5~3.6 Redis 和MongoDB
  • 第 4 章 与视图协同工作
    • 4.1 编写基于函数的视图和URL路由
    • 4.2 编写基于类的视图
    • 4.3 实现URL路由机制和基于产品的分页机制
    • 4.4 渲染至模板
    • 4.5 处理XHR请求
    • 4.6 使用装饰器处理请求
    • 4.7 处理自定义的404和500处理程序
    • 4.8 闪动消息以获得更好的用户反馈
    • 4.9 实现基于SQL的搜索机制
  • 第 5 章 WTForms
    • 5.1 将SQLAlchemy 模型数据表示为表单
    • 5.2 验证服务器端上的字段
    • 5.3 创建公共表单集合
    • 5.4 创建自定义字段和验证
    • 5.5 创建自定义Widget
    • 5.6 通过表单上传文件
    • 5.7 保护应用程序免受跨站点请求伪造(CSRF)

前言

官方文档

http://flask.pocoo.org/docs/0.12/ 英文
http://docs.jinkan.org/docs/flask/ 中文

代码资源

python -m参数

1,python xxx.py
2,python -m xxx.py

这是两种加载py文件的方式:
1叫做直接运行
2把模块当作脚本来启动(注意:但是__name__的值为’main’ )

  • 不能将应用程序文件保存为flask.py,否则 ,在导入时将与Flask 发生冲突

virtualenvwrapper

Python3.4以上版本不需要额外安装virtualenv安装包了,直接使用python -m venv env1即可创建虚拟环境

virtualenvwrapper 时一个基于virtualenv之上的工具,它将所欲的虚拟环境统一管理

安装如下命令:

$ sudo pip install virtualenvwrapper

virtualenvwrapper默认将所有的虚拟环境放在~/.virtualenvs目录下管理,可以修改环境变量WORKON_HOME来指定虚拟环境 的保存目录。

使用如下命令来启动virtualenvwrapper:

$ source /usr/local/bin/virtualenvwrapper.sh

还可以将该命令添加到/.bashrc/.profie等shell启动文件中,以便登陆shell后可直接使用virtualenvwrapper提供的命令。

参考这儿

MVC

为方便后续理解flask中的视图,模型等概念,这儿引入MVC

MVC框架的核心思想是:解耦,让不同的代码块之间降低耦合,增强代码的可扩展性和可移植性,实现向后兼容。

当前主流的开发语言如Java、PHP、Python中都有MVC框架。

Web MVC各部分的功能
M:全拼为Model,主要封装对数据库层的访问,对数据库中的数据进行增、删、改、查操作

V:全拼为View,用于封装结果,生成页面展示的html内容

C:全拼为Controller,用于接收请求,处理业务逻辑,与Model和View交互,返回结果

随记

1,route
route方法必须传入一个字符串形式的url路径,路径必须以斜线开始

url可以重复吗?视图函数可以重复吗?

url可以重复,url可以指定不同的请求方式

url 查找视图 从上往下执行,如果找到,不会继续匹配

视图函数不能重复,函数只允许有一个返回值

在flask中路由分发
1、如果路由名字后面没有写/   那么请求的路径后面就不可以写/
2、如果路由名字后面写了/  那么请求的路径后面怎么写都可以
# 所以在route()中路由名字一般情况下 /路由名字/
@app.route('/index/')
#    路由的名字和视图函数的名字 一般情况下一致,这个是开发的经验;
def index():
    return 'index'

2,返回JSON

在使用 Flask 写一个接口时候需要给客户端返回 JSON 数据,在 Flask 中可以直接使用 jsonify 生成一个 JSON 的响应

@app.route('/demo4')
def exp4():
    json_dict = {
        "user_id": 110,
        "user_name": "kenan"
    }
    return jsonify(json_dict)

不推荐使用 json.dumps 转成 JSON 字符串直接返回,因为返回的数据要符合 HTTP 协议规范,如果是 JSON 需要指定 content-type:application/json

第1章 配置Flask

1.2 处理基本的配置问题

配置设置方式
方法一

app = Flask(__name__)
app.debug=True

方法二

app.config["DEBUG"] = True

方法三

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

方法四 环境变量

$ export FLASK_DEBUG=1

方法五 多种方式
《Flask web 开发从入门到精通》读书笔记(上)_第1张图片
Flask可以自动选取大写形式的配置变量

from flask import Flask

app = Flask(__name__)
DEBUG = True
TESTING = True

另外还可以基于类的继承,在不通的环境(开发环境,测试环境,生产环境)中继承基类并覆写配置变量,基类配置不随环境改变的配置变量

密钥存储在单独的文件中,不属于版本控制系统的一部分

1.4 组织静态文件

文件目录

my_app/
	- app,py
	- config.py
	- __init__.py
	- static/
		- css/
		- js/
		- images/
			-logo.png

引用模板时

app = Flask(__name__,static_folder="/path/to/static/folder")

1.5 实例文件夹

my_app/
	- app.py
	- instance/
		- config.cfg
app = Flask(__name__,instance_path='instance/' instance_relative_config=True)
app.config.from_pyfile('config.cfg',silent=True)

所谓实例文件夹,是指和flaskr同级的一个名字为instance的文件夹,适合存放私有配置的秘钥或者本地数据库等不需要上传到Git的文件,可以通过Flask.instance_path获取完整路径。

1.6 视图和模型

这一节主要讨论怎么模块实现web的代码
具体代码就不贴了,主要记录以下各个文件完成的功能
文件组织目录

flask_app/

	- run.py # app.run()
	- my_app/
		- __init__.py # 创建app,注册register_blueprint(xxx)
		- hello/
			- __init__.py
			- models.py # 数据封装,数据库的增删改查
			- views.py # 封装结果生成页面展示内容生成 blueprint

通过app.env="development可以取消下面的注释
《Flask web 开发从入门到精通》读书笔记(上)_第2张图片

第2章 基于jinja2的模板机制

2.1 引导推荐的布局

flask_app/

	- run.py # app.run()
	- my_app/
		- __init__.py # 创建app,注册register_blueprint(xxx)
		- hello/
			- __init__.py
			- views.py 

view中的代码

from flask import render_template,request

@hello.route('/')
@hello.route('/home')
def hello_world():
	user = request.args.get('user','Shala')
    return render_template('index.html', user=user)
		- templates/ 放模板文件
			- index.html

当用户访问http://127.0.0.1:5000/hello?user=John,通过request.args.get('user','Shala')从request中获取然后随render_template 传递到渲染的模板上下文中

文档,Jinja2是Flask作者开发的模板系统。

2.2 实现块组合和继承布局

大型应用程序会有不同的界面,但界面的页眉,页脚相同
针对这种会有一个基本模板。
本书使用Bootstrap框架实现模板的简约设计

Bootstrap,来自 Twitter,是目前最受欢迎的前端框架。Bootstrap 是基于 HTML、CSS、JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷。

{% extends 'base.html' %}

{% block container %}
  <div class="top-pad">
    {% for id, product in products.items() %}
      <div class="well">
        <h2>
          <a href="{{ url_for('product.product', key=id) }}">{{ product['name'] }}</a>
          <small>$ {{ product['price'] }}</small>
        </h2>
      </div>
    {% endfor %}
  </div>
{% endblock %}

url_for() 与蓝图结合使用
url_for() 的第一个参数代表product蓝图下的product函数,
url_for() 的第二个参数 key=id ,是指:product 蓝图下的 product 函数的参数 key参数 。

参考如下:

from werkzeug.exceptions import abort
from flask import render_template,Blueprint
from my_app.product.models import PRODUCTS
#注册一个名字为product的蓝图
product_blueprint = Blueprint('product', __name__)

@product_blueprint.route('/product/')
def product(key):
    product = PRODUCTS.get(key)
    if not product:
        abort(404)
    return render_template('product.html', product=product)

2.3 自定义上下文处理器

逻辑处理行为(比如某些数值计算)应在视图中完成,以此保持模板的整洁
上下文处理器可以将数值传递至某个方法中,并处理后返回值

@product_blueprint.context_processor
def some_processor():
    def full_name(product):
        return f"{product['category']} / {product['name']}"
    return {'full_name': full_name}

html中使用,按照{{ full_name(product) }} 这种方式使用上下文处理器

{% extends 'home.html' %}

{% block container %}
  <div class="top-pad">
    <h4>{{ full_name(product) }}</h4>
    <h1>{{ product['name'] }}
      <small>{{ product['category'] }}</small>
    </h1>
    <h3>{{ product['price']|format_currency }}</h3>
  </div>
{% endblock %}

2.4 创建自定义 jinja2 过滤器

  • 在blueprint 级别创建过滤器app_template_filter
@product_blueprint.app_template_filter('full_name')
def full_name_filter(product):
    return f"{product['category']} / {product['name']}"

html中使用

{{product | full_name}}
  • 在应用程序级别创建过滤器template_filter
import ccy
from flask import Flask, request
from my_app.product.views import product_blueprint

app = Flask(__name__)
app.register_blueprint(product_blueprint)

@app.template_filter('format_currency')
def format_currency_filter(amount):
	currency_code = ccy.countryccy(request.accept_languages.best[-2:]) or 'USD'
	return '{0} {1}'.format(currency_code, amount)

accept_languages返回浏览器的语言环境,是一个字符串
html中使用

<h3>{{ product['price']|format_currency }}</h3>

2.5 为表单创建自定义宏

部分内容来自此处
Jinja2中的宏功能有些类似于传统程序语言中的函数,跟python中的函数类似,可以传递参数,但不能有返回值,有声明和调用两部分。让我们先声明一个宏:
_helper.html 中输入以下内容

 <!--宏定义-->
{% macro input(name, type='text', value='') -%}
    <input type="{{type}}" name="{{name}}" value="{{value|e}}">
{%- endmacro %}

%之前和之后的-号将去除这些块之前和之后的空格

上面的代码定义了一个宏,宏定义要加macro,宏定义结束要加endmacro标志。宏的名称就是input,它有3个参数,分别是name、type和value,后两个参数有默认值。调用时用下面这个表达式:
宏被导入文件

{% from '_helper.html' import input %}

以下代码调用宏

<p>用户名:{{ input('username') }}</p>
<p>密  码:{{ input('password', type='password') }}</p>

如果要编写一个无法从当前文件外部访问的私有宏,可在该宏的名称前加上下划线(_

2.6 高级日期和时间格式

JavaScript 日期处理类库Moment.js-中文
Moment.js-英文
下载moment.min.js后放在static/js文件夹中
引用

<script src="{{ url_for('static', filename='js/moment.min.js') }}"></script>

但是看到了这个Moment.js 宣布停止开发,现在该用什么?

第3章 Flask中的数据模型

3.1 创建SQLAchemy DB 实例

pip install flask-sqlalchemy

关于连接数据库的URL,可以访问-英文
关于连接数据库的URL,可以访问-中文

需要对特殊字符(如密码中可能使用的字符)进行URL编码才能正确解析。 。以下是包含密码的URL示例 “kx%jj5/g” ,其中%/字符表示为 %25%2F ,分别为:

postgresql+pg8000://dbuser:kx%25jj5%2Fg@pghost10/appdb

可以使用以下命令生成上述密码的编码 urllib.parse

>>>import urllib.parse
>>>urllib.parse.quote_plus("kx%jj5/g")
>>>'kx%25jj5%2Fg'

在上述连接 介绍MYSQL 的部分,有提到数据库断开的问题,这个当年做项目也是有遇到过,很头大,当时百度也不知道怎么解决,现在这儿有遇到

不希望db实例绑定到单个应用程序上,希望在多个应用程序间加以使用或者采用动态的创建应用程序。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def creat_app():
    app = Flask(__name__)
    db.init_app(app)
    return app

3.2 创建基本的模型

创建数据库模型Product 类似下面这样

class Product(db.Model):

    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(255))
    price = db.Column(db.Float)

    def __init__(self,name,price):
        self.name = name
        self.price = price

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

使用方式一

@catalog.route('/product/')
def product(key):
    product = Product.objects.get_or_404(key=key)
    return f"Product - {product.name}, ${product.price}"

JSON返回

@catalog.route('/products')
def products():
    products = Product.objects.all()
    res = {}
    for product in products:
        res[product.key] = {
            'name': product.name,
            'price': str(product.price),
        }
    return jsonify(res)

3.3 创建关系分类模型

这一届主要理解db.relationship

这个函数有点难用,一是因为它的有几个参数不太好理解,二是因为它的参数非常丰富,让人望而却步。下面通过一对多、多对一、多对多几个场景下 relationship 的使用,来一步步熟悉它的用法。

参考这儿

3.4 利用 Alembic 和 Flask-Migrate 迁移数据库

安装Alembic

pip install Flask-Migrate

具体使用可以参考下面的文章,先收藏,用的时候再看
参考1
参考2

3.5~3.6 Redis 和MongoDB

pip install redis
pip install flask-mongoengine

指定使用“my_catalog”

app = Flask(__name__)
app.config['MONGODB_SETTINGS'] = {'DB': 'my_catalog'}
app.debug = True
db = MongoEngine(app)

对于SQLAlchemy 对应的类为db.Model,对于MongoDB 对应的类则为db.Document

class Product(db.Document):
    created_at = db.DateTimeField(
        default=datetime.datetime.now, required=True
    )
    key = db.StringField(max_length=255, required=True)
    name = db.StringField(max_length=255, required=True)
    price = db.DecimalField()

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

使用

@catalog.route('/product-create', methods=['POST',])
def create_product():
    name = request.form.get('name')
    key = request.form.get('key')
    price = request.form.get('price')
    product = Product(
        name=name,
        key=key,
        price=Decimal(price)
    )
    product.save()
    return 'Product created.'

第 4 章 与视图协同工作

4.1 编写基于函数的视图和URL路由

1,GET请求(默认)

@app.route('/category-create')
def create_category():
    name = request.args.get('name')

2, POST请求,需要指定methods,通过form获取参数,因为POST请求假设数据是以表单的方式提交的

@catalog.route('/category-create', methods=['POST',])
def create_category():
    name = request.form.get('name')

3,合并的请求

@catalog.route('/category-create', methods=['GET','POST',])
def create_category():
	if request.method == 'GET':
    	name = request.args.get('name')
    else:
    	name = request.form.get('name')

如果尝试向仅支持POST请求的方法中发送GET请求,请求将失败,并生成405 HTTP错误,反过来也一样

add_url_rule使用

def get_request():
	bar = request.args.get('foo','bar')
	return 'xxxxx'

app = Flask(__name__)
app.add_url_rule('/a-get-request',view_func=get_request)

4.2 编写基于类的视图

参考自此链接
flask提供了一个名为View的类,可以继承该类进而添加自定义的行为

之前我们接触的视图都是函数,所以一般简称视图函数。其实视图也可以基于类来实现,类视图的好处是支持继承,但是类视图不能跟函数视图一样,写完类视图还需要通过app.add_url_rule(url_rule,view_func来进行注册。以下将对两种类视图进行讲解

标准类视图是继承自flask.views.View,并且在子类中必须实现dispatch_request方法,这个方法类似于视图函数,也要返回一个基于Response或者其子类的对象。以下将用一个例子进行讲解:

from flask.views import View
class PersonalView(View):
    def dispatch_request(self):
        return "知了课堂"
# 类视图通过add_url_rule方法和url做映射
app.add_url_rule('/users/',view_func=PersonalView.as_view('personalview'))

Flask还为我们提供了另外一种类视图flask.views.MethodView,对每个HTTP方法执行不同的函数(映射到对应方法的小写的同名方法上),以下将用一个例子来进行讲解

class LoginView(views.MethodView):
    # 当客户端通过get方法进行访问的时候执行的函数
    def get(self):
        return render_template("login.html")

    # 当客户端通过post方法进行访问的时候执行的函数
    def post(self):
        email = request.form.get("email")
        password = request.form.get("password")
        if email == '[email protected]' and password == '111111':
            return "登录成功!"
        else:
            return "用户名或密码错误!"

# 通过add_url_rule添加类视图和url的映射,并且在as_view方法中指定该url的名称,方便url_for函数调用
app.add_url_rule('/myuser/',view_func=LoginView.as_view('loginview'))

4.3 实现URL路由机制和基于产品的分页机制

URL路由的转换

@qpp.rout('/test/')
def get_name(name):
	return name

包含特定长度的字符串

@qpp.rout('/test/')
def get_name(code):
	return code

解析整数,或者指定所接收的最小和最大值,也可以用float代替int

@app.rout('test/int(min=18,max=99):age')
@qpp.rout('/test/')
def get_name(age):
	return str(age)

分页。返回每页前10件产品

@catalog.route('/products')
@catalog.route('/products/')
def products(page=1):
    products = Product.query.paginate(page, 10)
    return render_template('products.html', products=products)

Flask-SQLAlchemy 提供的 paginate 方法。页数是 paginate() 方法的第一个参数,也是唯一必需的参数。可选参数 per_page 用来指定 每页显示的记录数量;如果没有指定,则默认显示 20 个记录。另一个可选参数为 error_out,当其设为 True 时(默认值),如果请求的页数超出了范围,则会返回 404 错误;如果 设为 False,页数超出范围时会返回一个空列表。

4.4 渲染至模板

这一节中提到了request.endpoint ,有必要了解一下

4.5 处理XHR请求

异步JavaScript XMLHttpRequest (XHR) 也称作Ajax

if request.is_xhr:
	xxxx
	return jsonify(xxx)

4.6 使用装饰器处理请求

书上这儿的示例代码缩进有问题,看了半天看不懂参考此链接

from functools import wraps

def template_or_json(template=None):
    """Return a dict from your view and this will either pass it to a template or render json. Use like:
    @template_or_json('template.html')
    """

    def decorated(f):
        @wraps(f)
        def decorated_fn(*args, **kwargs):
            ctx = f(*args, **kwargs)
            if request.is_xhr or not template:
                return jsonify(ctx)
            else:
                return render_template(template, **ctx)
        return decorated_fn
    return decorated 

这个装饰器做的就是之前小节中我们对 XHR 的处理,即检查请求是否是 XHR,根据结果是否决定是渲染模板还是返回 JSON 数据。
使用

@app.route('/')
@app.route('/home')
@template_or_json('home.html')
def home():
    products = Product.query.all()
    return {'count': len(products)} 

4.7 处理自定义的404和500处理程序

Flask 对象 app 有一个叫做 errorhandler()的方法,这使得处理应用程序错误的方式更加美观和高效。

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404 

出现报错 将看到渲染后的模板
《Flask web 开发从入门到精通》读书笔记(上)_第3张图片

4.8 闪动消息以获得更好的用户反馈

案例:当用户创建完产品并被重定向至新生成的产品时,较好的方法是通知用户该产品已经被创建完毕
会话依赖于密钥,因此先添加密钥

app.secret_key = 'some_random_key' 

使用
注意 flash 消息,它提醒用户一个商品创建成功了。flash()的第一个参数是要被显示的消息,第二个参数是消息的类型。

from flask import flash,redirect

@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
    if request.method == "POST":
        name = request.form.get('name')
        price = request.form.get('price')
        categ_name = request.form.get('category')
        category = Category.query.filter_by(name=categ_name).first()
        if not category:
            category = Category(categ_name)
        product = Product(name, price, category)
        flash('The product %s has been created' % name, 'success')
        return redirect(url_for('catalog.product', id=product.id))
    return render_template('product-create.html') 

当然对应的模板中也要修改,以适应闪动消息
最后的效果类似于
《Flask web 开发从入门到精通》读书笔记(上)_第4张图片

4.9 实现基于SQL的搜索机制

join 数据库表的连接查询

from sqlalchemy.orm.util import join
@catalog.route('/product-search')
@catalog.route('/product-search/')
def product_search(page=1):
    name = request.args.get('name')
    price = request.args.get('price')
    company = request.args.get('company')
    category = request.args.get('category')
    products = Product.query
    if name:
        products = products.filter(Product.name.like('%' + name + '%'))
    if price:
        products = products.filter(Product.price == price)
    if company:
        products = products.filter(Product.company.like('%' + company + '%'))
    if category:
        products = products.select_from(join(Product, Category)).filter(Category.name.like('%' + category + '%'))
    return render_template(
        'products.html', products=products.paginate(page, 10) ) 

第 5 章 WTForms

WTForms 为许多字段提供了服务器端验证,从而提高了开发速度并减少了所需的总体工作量

$ pip install Flask-WTF 

5.1 将SQLAlchemy 模型数据表示为表单

文档

from flask_wtf import FlaskForm
from wtforms import StringField, DecimalField, SelectField

class ProductForm(FlaskForm):
    name = StringField('Name')
    price = DecimalField('Price')
    category = SelectField('Category', coerce=int) 

Category字段里有一个叫做 coerce 的参数(表示为可选列表),意味着在任何验证或者处理之前强制转化来自HTML表单的输入为一个整数。在这里,强制仅仅意味着转换,由一个特定数据类型到另一个不同的数据类型。

通过Flask-WTF来保护表单免受CSRF攻击
单个表单禁用:生成表单时加入参数csrf_enabled=False
实现方式

from my_app.catalog.models import ProductForm

@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
    form = ProductForm(csrf_enabled=False)
    categories = [(c.id, c.name) for c in Category.query.all()]
    form.category.choices = categories
    if request.method == 'POST':
        name = form.name.data
        price = form.price.data
        category = Category.query.get_or_404(
            form.category.data
        )
        product = Product(name, price, category)
        db.session.add(product)
        db.session.commit()
        flash('The product %s has been created' % name, 'success')
        return redirect(url_for('catalog.product', id=product.id))
    return render_template('product-create.html', form=form) 

5.2 验证服务器端上的字段

在 WTForm 字段中很容易添加验证机制。我们仅仅需要传递一个 validators 参数

from decimal import Decimal
from wtforms.validators import InputRequired, NumberRange

class ProductForm(FlaskForm):
    name = StringField('Name', validators=[InputRequired()])
    price = DecimalField('Price', validators=[
        InputRequired(), NumberRange(min=Decimal('0.0'))
    ])
    category = SelectField(
        'Category', validators=[InputRequired()], coerce=int) 

InputRequired意味着字段不可缺少,否则对应表单不会被提交
price 字段设置价格不能小于0

@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
    form = ProductForm(csrf_enabled=False)
	categories = [(c.id, c.name) for c in Category.query.all()]
    form.category.choices = categories
    if request.method == 'POST' and form.validate():
        name = form.name.data
        price = form.price.data
        category = Category.query.get_or_404(form.category.data )
        product = Product(name, price, category, filename)
        db.session.add(product)
        db.session.commit()
        flash('The product %s has been created' % name, 'success')
        return redirect(url_for('catalog.product', id=product.id))

    if form.errors:
        flash(form.errors, 'danger')

    return render_template('product-create.html', form=form)

form.errors的闪动行为仅显示JSON对象形式的错误信息
《Flask web 开发从入门到精通》读书笔记(上)_第5张图片
也可以直接用if form.validate_on_submit(): 代替if request.method == 'POST' and form.validate():效果一样

5.3 创建公共表单集合

设置公共表单于随后在需要时加以复用,共有的NameForm单独设置,ProductFormCategoryForm复用的直接继承

class NameForm(FlaskForm):
    name = StringField('Name', validators=[InputRequired()])

class ProductForm(NameForm):
    price = DecimalField('Price', validators=[
        InputRequired(), NumberRange(min=Decimal('0.0'))])
    category = CategoryField(
        'Category', validators=[InputRequired()], coerce=int)

class CategoryForm(NameForm):
    pass

5.4 创建自定义字段和验证

这一节实现的功能主要是,有些字段可以自动从数据库中获取并生成可选字段

class CategoryField(SelectField):

    def iter_choices(self):
        categories = [(c.id, c.name) for c in Category.query.all()]
        for value, label in categories:
            yield (value, label, self.coerce(value) == self.data)

    def pre_validate(self, form):
        for v, _ in [(c.id, c.name) for c in Category.query.all()]:
            if self.data == v:
                break
        else:
            raise ValueError(self.gettext('Not a valid choice'))


class ProductForm(NameForm):
    price = DecimalField('Price', validators=[
        InputRequired(), NumberRange(min=Decimal('0.0'))
    ])
    category = CategoryField(
        'Category', validators=[InputRequired()], coerce=int
    )
    image = FileField('Product Image', validators=[FileRequired()])

SelectField 中本身有一个iter_choices 方法,这儿覆写iter_choices方法,直接从数据库中获取分类值,无需在每次使用该表单时填写对应的字段


CategoryField 也可通过QuerySlectField予以实现, WTForms 3.0.之后删除了WTForms 扩展,很多扩展变成了单独的库,其中WTForms-SQLAlchemy 中实现了QuerySelectField


因此可以移除用于表单分类的下面2句

categories = [(c.id, c.name) for c in Category.query.all()]
form.category.choices = categories

不希望支持重复的分类,可以在表单上使用自定义验证器

def check_duplicate_category(case_sensitive=True):
    def _check_duplicate(form, field):
        if case_sensitive:
            res = Category.query.filter(
                Category.name.like('%' + field.data + '%')
            ).first()
        else:
            res = Category.query.filter(
                Category.name.ilike('%' + field.data + '%')
            ).first()
        if res:
            raise ValidationError(
                'Category named %s already exists' % field.data
            )
    return _check_duplicate

关于 likeilike操作符可以模糊匹配字符串,like是一般用法,ilike匹配时则不区分字符串的大小写

5.5 创建自定义Widget

文档

from wtforms.widgets import html_params, Select, HTMLString
class CustomCategoryInput(Select):

    def __call__(self, field, **kwargs):
        kwargs.setdefault('id', field.id)
        html = []
        for val, label, selected in field.iter_choices():
            html.append(
                ' %s' % (
                    html_params(
                        name=field.name, value=val, checked=selected, **kwargs
                    ), label
                )
            )
        return HTMLString(' '.join(html))


class CategoryField(SelectField):
    widget = CustomCategoryInput()
    pass

5.6 通过表单上传文件

需要向应用配置提供一个参数:UPLOAD_FOLDER。这个参数告诉 Flask 上传文件被存储的位置。

图片不建议 存储到数据库,应始终将图像和其他上传内容存储在文件系统中,并使用字符串字段将其位置存储在数据库中

import os
from flask import Flask
# 指定接收的文件格式
ALLOWED_EXTENSIONS = set(['txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.realpath('.') + '/my_app/static/uploads' # 这个路径必须先创建好,否则报错

增加下面内容

from flask_wtf.file import FileField, FileRequired

class ProductForm(NameForm):
    image = FileField('Product Image', validators=[FileRequired()])
from my_app import db, app, ALLOWED_EXTENSIONS
from werkzeug import secure_filename
import os

def allowed_file(filename):
    return '.' in filename and \
            filename.lower().rsplit('.', 1)[1] in ALLOWED_EXTENSIONS

@catalog.route('/product-create', methods=['GET', 'POST'])
def create_product():
    form = ProductForm()

    if form.validate_on_submit():
        name = form.name.data
        price = form.price.data
        category = Category.query.get_or_404(
            form.category.data
        )
        image = form.image.data
        if allowed_file(image.filename):
            filename = secure_filename(image.filename)
            image.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
        product = Product(name, price, category, filename)
        db.session.add(product)
        db.session.commit()
        flash('The product %s has been created' % name, 'success')
        return redirect(url_for('catalog.product', id=product.id))

    if form.errors:
        flash(form.errors, 'danger')

    return render_template('product-create.html', form=form)

secure_filename()函数来检查文件名

要注意的是,secure_filename仅返回ASCII字符。所以, 非ASCII(比如汉字)会被过滤掉,空格会被替换为下划线。你也可以自己处理文件名,或是在使用这个函数前将中文替换为拼音或是英文。

对应的HTML中也要修改,表单应该包含参数enctype="multipart/form-data",以便告诉应用该表单参数含有多个数据。

5.7 保护应用程序免受跨站点请求伪造(CSRF)

Flask 默认不提供任何 CSRF 保护,且需要在表单验证级别上予以处理。当前案例将通过Flask-WTF 扩展处理这些。
Flask-WTF 默认提供CSRF,只需移除相关语句即可开发CSRF防护
form = ProductForm(csrf_enabled=False) 变为form = ProductForm()
相应的需要调整配置

app.config['WTF_CSRF_SECRET_KEY'] = 'random key for form' 

你可能感兴趣的:(笔记,flask,前端,python)