http://flask.pocoo.org/docs/0.12/
英文
http://docs.jinkan.org/docs/flask/
中文
1,python xxx.py
2,python -m xxx.py
这是两种加载py文件的方式:
1叫做直接运行
2把模块当作脚本来启动(注意:但是__name__的值为’main’ )
flask.py
,否则 ,在导入时将与Flask 发生冲突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提供的命令。
参考这儿
为方便后续理解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
配置设置方式
方法一
app = Flask(__name__)
app.debug=True
方法二
app.config["DEBUG"] = True
方法三
if __name__=='__main__':
app.run(debug=True)
方法四 环境变量
$ export FLASK_DEBUG=1
from flask import Flask
app = Flask(__name__)
DEBUG = True
TESTING = True
另外还可以基于类的继承,在不通的环境(开发环境,测试环境,生产环境)中继承基类并覆写配置变量,基类配置不随环境改变的配置变量
密钥存储在单独的文件中,不属于版本控制系统的一部分
文件目录
my_app/
- app,py
- config.py
- __init__.py
- static/
- css/
- js/
- images/
-logo.png
引用模板时
app = Flask(__name__,static_folder="/path/to/static/folder")
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获取完整路径。
这一节主要讨论怎么模块实现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_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作者开发的模板系统。
大型应用程序会有不同的界面,但界面的页眉,页脚相同
针对这种会有一个基本模板。
本书使用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)
逻辑处理行为(比如某些数值计算)应在视图中完成,以此保持模板的整洁
上下文处理器可以将数值传递至某个方法中,并处理后返回值
@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 %}
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>
部分内容来自此处
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>
如果要编写一个无法从当前文件外部访问的私有宏,可在该宏的名称前加上下划线(_
)
JavaScript 日期处理类库Moment.js-中文
Moment.js-英文
下载moment.min.js
后放在static/js
文件夹中
引用
<script src="{{ url_for('static', filename='js/moment.min.js') }}"></script>
但是看到了这个Moment.js 宣布停止开发,现在该用什么?
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
创建数据库模型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)
这一届主要理解db.relationship
这个函数有点难用,一是因为它的有几个参数不太好理解,二是因为它的参数非常丰富,让人望而却步。下面通过一对多、多对一、多对多几个场景下 relationship 的使用,来一步步熟悉它的用法。
参考这儿
安装Alembic
pip install Flask-Migrate
具体使用可以参考下面的文章,先收藏,用的时候再看
参考1
参考2
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.'
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)
参考自此链接
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'))
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,页数超出范围时会返回一个空列表。
这一节中提到了request.endpoint
,有必要了解一下
异步JavaScript XMLHttpRequest (XHR) 也称作Ajax
if request.is_xhr:
xxxx
return jsonify(xxx)
书上这儿的示例代码缩进有问题,看了半天看不懂参考此链接
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)}
Flask 对象 app 有一个叫做 errorhandler()
的方法,这使得处理应用程序错误的方式更加美观和高效。
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
案例:当用户创建完产品并被重定向至新生成的产品时,较好的方法是通知用户该产品已经被创建完毕
会话依赖于密钥,因此先添加密钥
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')
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) )
WTForms 为许多字段提供了服务器端验证,从而提高了开发速度并减少了所需的总体工作量
$ pip install Flask-WTF
文档
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)
在 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对象形式的错误信息
也可以直接用if form.validate_on_submit():
代替if request.method == 'POST' and form.validate():
效果一样
设置公共表单于随后在需要时加以复用,共有的NameForm
单独设置,ProductForm
和CategoryForm
复用的直接继承
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
这一节实现的功能主要是,有些字段可以自动从数据库中获取并生成可选字段
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
关于 like
和 ilike
操作符可以模糊匹配字符串,like
是一般用法,ilike
匹配时则不区分字符串的大小写
文档
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
需要向应用配置提供一个参数: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"
,以便告诉应用该表单参数含有多个数据。
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'