Google+社群——Hello, Flask!

为了方便讨论和交流,创建了一个Google+社群。现在可以在社群里分享信息,创建问题和讨论。欢迎加入!

关于提问

专栏关注人数增多以后,提问也越来越多。但是有些问题我完全不知道问的是什么……学会提问很重要!

遇到问题后应该有这么一个解决流程:

检查代码是否有语法错误——查看相应的源码和文档——Google搜索——如果到了这一步还没法解决,再到论坛上发帖(建议到StackOverflow上提问)。

问题应该尽量包括下面的内容:

  1. 期望效果
  2. 实际效果
  3. 你的操作步骤和尝试过的解决办法
  4. 相关的代码和错误输出
  5. 操作系统和语言、库等的版本

最后还要注意排版,内容尽量简洁,措辞礼貌一些。

因为Google+社群里不好放代码,可以在StackOverflow或知乎上提问,粘贴链接过来,或是在Github上创建Gist

 

Google+社群

你可以在这里分享关于Flask、Python以及Web开发的一切信息,说点儿想说的,不用拘束。

plus.google.com/u/0/com

Flask文件上传系列教程

文章目录

1、Flask文件上传(一):原生实现

    • 上传配置
    • 安全问题

2、Flask文件上传(二):使用扩展实现

    • 使用Flask-Uploads简化上传过程
    • 大型项目里Flask-Uploads的配置

3、Flask文件上传(三):完整实现

    • 使用Flask-WTF创建上传表单
    • 分离模板文件

4、Flask文件上传(四):文件管理与多文件上传

  • 文件管理
  • 文件名处理
  • 中文文件名问题
  • 多文件上传

5、Flask文件上传(五):拖拽上传和进度条

    • 使用插件集成进度条等功能
    • 使用Pillow生成图像缩略图

 

项目Demo

1、简单的文件管理系统:helloflask/cloud-drive

    • 文件上传
    • 文件删除
    • 图片预览

2、完善的文件管理系统:greyli/flask-file-uploader

    • 拖拽上传
    • 进度条
    • 文件管理
    • Bootstrap样式
    • 图片预览
    • 文件信息

 

 

优质信息源推荐

编程学习中,有两种信息,一种是静态的:官方文档,系统教程,参考书,技术书;另一种是动态的:各种优秀的(非纯技术)书,经验总结,发明创造甚至是奇技淫巧。静态的信息建立你的根基,让你强大;动态的信息塑造你的思想和价值观,让你少走弯路,变得灵活。

今天我来推荐三个动态的信息源:一个网站,两个周刊。

 

网站

Hacker News

我没有多少时间去获取资讯。如果你不是躺在养老院里,也不应该投入过多的时间到资讯上,况且大多数资讯根本不值得阅读。但适当的资讯是必要的,这让你增进对世界的了解。Hacker News可以作为每日资讯的获取来源,不仅仅是编程方面的内容,还会有新技术,网站,新闻,项目,书等等。

顺便推荐Hacker News创始人Paul Graham的书《黑客与画家》,书里文章选自他的网站http://paulgraham.com/

 

周刊

1、Fullweb Weekly:The newsletter for fullstack developers

2、Pycoder’s Weekly: A Weekly Python E-Mail Newsletter

这里面有前后端和Python方面的新动态,优秀的库,巧妙的实践,基础知识,各种文章和Tips。尽管内容很多很丰富,但是你应该在打好基础后再来读这些东西。

如果你有大把时间(我想你并没有),可以看看DesignerNewsReddit的Programming板块,以及这个关于Python资讯的列表

 

JavaScript计算器

用JavaScript做了一个计算器,大部分时间都花在完善样式和交互上了,现在还想着再给它添加一个主题。我发现,我喜欢把一个东西从丑变美的过程,好看比好用更吸引我。这不是一个好习惯……calculator
Demo:https://greyli.github.io/calculator/
源码:https://github.com/greyli/calculator

 

优化交互、美化样式

在这个计算器里,用到了一些处理技巧,可以让它看起来更真实和漂亮。

计算器边缘阴影

这里使用box-shadow的inset方法,也就是把阴影内嵌到元素里,让计算器看起来是有厚度的:

box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), inset -1px -6px 12px 0.1px #89847e;

参考内容:https://css-tricks.com/snippets/css/css-box-shadow/

按钮按下效果

其实是设置按钮的box-shadow,按下时把box-shadow设为none,同时按钮向下移动

button {
  box-shadow: 1px 2px #666;
}

button:active {
  box-shadow: none;
  transform: translateY(4px);
}

按钮上的字不可选择

双击按钮或是拖动按钮选择会出现蓝色背景色,设置user-select去掉这个特性

.un-selectable {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

去掉按钮被选中后的蓝色边线

button:focus {outline:0;}  /* 设为none效果相同,或加上 !important */

 

换域名小记

起名字

起名字是个简单但让人头疼的事情。

你想要一个独特的名字,但又不想透露太多的个人信息。既不能太土,又不能太长,不能太难读,也不能太难记,而且最要命的是,如果你想在所有社交网站都有一个相同的id,这太难了——好名字全被注册完了。

连续换了两个域名后,我想这次终于遇到对我来说完美的域名了——greyli.com

 

换域名

更改完DNS设置后,在数据库里用新域名替换掉旧域名。

和上次同时换主机和域名不同,这次只需要在数据库里改动一些内容就完成了:

wp_options SET option_value = replace(option_value, 'www.mydomain.com','www.newdomain.com');
UPDATE wp_posts SET post_content = replace(post_content, 'www.mydomain.com','www.newdomain.com');
UPDATE wp_comments SET comment_content = replace(comment_content, 'www.mydomain.com', 'www.newdomain.com');
UPDATE wp_comments SET comment_author_url = replace(comment_author_url, 'www.mydomain.com', 'www.newdomain.com');
 
 
文章里有些地址是完整的,所以可能还需要加上下面这条语句:
UPDATE wp_posts SET post_content = replace(post_content, 'http://www.olddomain.com','http://www.newdomain.com');
 
 

Flask项目配置(Configuration)

在Flask项目中,我们会用到很多配置(Config)。比如说设置秘钥,设置数据库地址,像下面这样:

...
app.config['SECRET_KEY'] = 'some strange words'

Flask的配置对象(config)是一个字典的子类(subclass),所以你可以把配置用键值对的方式存储进去。这是一个通用的处理接口,Flask内置的配置,扩展提供的配置,你自己的配置,都集中在一处。

为什么需要使用自己的配置?
假设你在做一个博客,有十个视图函数都定义了每页显示的文章数。当你写好以后,发现每页的文章太多,想把这个值改小一点,这时你要找到这十个视图函数,分别修改这个值,很蠢吧?使用配置你就使用一行控制所有的变量:

app.config['POST_PER_PAGE'] = 12

在十个视图函数里使用配置变量代替固定值:

post_per_page = app.config['POST_PER_PAGE']

其实不就是设置了一个变量嘛……

你有两种方式来设置配置:

  1. 直接写出配置的值,像上面那样。
  2. 对于不适合写在程序里的配置,比如密码等,需要把配置写入系统环境变量,然后使用os模块提供的方法获取:
set MAIL_USERNAME=me@greyli.com  # windows
export MAIL_USERNAME=me@greyli.com  # *unix

获取变量并写入:

import os

from flask import Flask

app = Flask(__name__)
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')

如果你使用虚拟环境,设置环境变量时注意要激活虚拟环境,同时不要给变量值加引号。

set MAIL_USERNAME=me@greyli.com  # 结果是'me@greyli.com'
set MAIL_USERNAME='me@greyli.com'  # 结果是"'me@greyli.com'"

 

你有三种方式来处理配置。

直接写入主脚本

当你的程序很小的时候,可以直接把配置写在主脚本里:

from flask import Flask

app = Flask(__name__)
app.config['SECRET_KEY'] = 'some secret words'
app.config['DEBUG'] = True
app.config['ITEMS_PER_PAGE'] = 10

使用字典的update方法可以简化代码:

from flask import Flask

app = Flask(__name__)
app.config.update(
    DEBUG=True,
    SECRET_KEY='some secret words',
    ITEMS_PER_PAGE=10
)

 

单独的配置文件

程序逐渐变大时,配置也逐渐增多,写在主脚本里太占地方,不够优雅(这时你应该已经把表单,路由,数据库模型等等分成独立的文件了。关于大型项目结构,后续会总结)。我们可以创建一个单独的配置文件。和上面同样的配置,现在可以改写为:

config.py

SECRET_KEY = 'some secret words'
DEBUG = True
ITEMS_PER_PAGE = 10

在创建程序实例后导入配置:

import config

...
app = Flask(__name__)
app.config.from_object(config)
...

 

创建不同的配置类

大型项目需要多个配置组合,比如开发时的配置,测试的配置,部署的配置……这样我们需要在配置文件里创建不同的配置类,然后在创建程序实例时引入相应的配置类。

最佳实践是创建一个存储通用配置的基类,然后为不同的使用使用场景创建新的继承基类的配置类:

config.py(这里为开发和测试创建了不同的数据库)

import os
basedir = os.path.abspath(os.path.dirname(__file__))


class Config:  # 基类
    SECRET_KEY = 'some secret words'
    ITEMS_PER_PAGE = 10


class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
    WTF_CSRF_ENABLED = False


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,

    'default': DevelopmentConfig
}

 

通过from_object()方法导入配置:

from config import config  # 导入存储配置的字典

...
app = Flask(__name__)
app.config.from_object(config['development'])  # 获取相应的配置类
...

 

关于大型项目结构,扩展的初始化,使用程序工厂函数创建程序实例等内容见后续。

 

相关链接

 

– – – – –

更多关于Flask和Web开发的优质原创内容,欢迎关注Hello, Flask! – 知乎专栏

Flask实践:待办事项(ToDo-List)

这一次我用Flask实现了一个待办事项(To-Do List)应用。这篇文章主要介绍这个应用的大致实现过程和一些让交互更简洁的处理技巧。这个App用了这篇文章里提到的Materialize框架。

程序界面

程序界面

Demo体验:http://task5.herokuapp.com/

简化模板

不论是从用户体验的角度,还是从减少工作量,提高加载速度等方面考虑,页面和跳转应该尽可能的少一些,尤其是这样的工具类应用。这可以通过优化模板实现,也可以通过JavaScript实现。

举两个例子。

一、编辑条目

我找到了另一个使用Flask实现的To-Do List应用,做个参照。在这个应用里,加上基模板,他一共用了4个页面。

Demo:http://flask-todoapp.herokuapp.com/

编辑一个条目的流程是这样的:

  1. 点击编辑按钮,页面跳转到另一个模板
  2. 渲染出表单,编辑内容
  3. 提交后跳转回主页面。

当你点击新建或是编辑条目/分类时,会跳转到另一个页面:

跳转到新页面编辑条目

跳转到新页面编辑条目

这其实只要一个页面就够了。

借助jQuery,我们可以这样实现:

  1. 渲染主页面的条目时,为每一个条目附加一个编辑表单,使用CSS隐藏它(display: none)
  2. 点击编辑按钮时,隐藏条目,显示表单(使用jQuery的hide和show函数)
  3. 表单提交后跳转到原页面
编辑条目

编辑条目

这样一来,就只需要一个页面了。在续篇里,使用AJAX技术在后台传送数据,体验会更好。

二、删除条目

想象一下,你在豆瓣收藏了很多喜欢的条目,你想清理一下,在页面最底端你删除了一个条目,如果这时页面在删除后跳转,就回到页面顶端了,可你还想删除在底端的另一些条目。因为删除不用传递数据,我们可以用这个小技巧解决:

在页面上,点击删除按钮后,使用jQuery的slideUp()函数隐藏条目,在后台仍然指定相应的视图函数,但是这个函数不跳转到任何页面(只是在数据库里删除条目),只返回下面的204状态码(代表无内容)。

return (''), 204

这样删除条目的体验会更加流畅和自然。

 

数据库框架Flask-SQLAlchemy

因为程序很小,我这次仍然把配置选项放在单个文件里。这次使用了数据库框架Flask-SQLAlchemy,数据库使用SQLite。和其他扩展一样,安装后初始化并配置数据库:

from flask_sqlalchemy import SQLAlchemy

basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__)
app.config['SECRET_KEY'] = 'a secret string'
app.config['SQLALCHEMY_DATABASE_URI'] = \
   'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)

解释一下这里的配置选项:

  • SQLALCHEMY_DATABASE_URI:这里要注意拼写,不要把URI写成URL。前一个是通用资源表示号(Universal Resource Identifier),后者是统一资源定位符(Uniform Resource Locator)。
  • SQLALCHEMY_COMMIT_ON_TEARDOWN:按字面理解就是,在每次请求结束后自动提交数据库会话,也就是db.session.commit()。
    • 在2.0版本里,这个配置选项被认为有害,从文档里删掉了,在这个issue里讨论了这个问题。如果不想手动提交数据库会话,你可以继续使用它。
    • 但是有时候,你需要手动提交,比如在这个应用里,有一个创建新的分类的函数,创建后使用redirect函数跳转到这个新建的分类(需要分类的id作为参数),如果不提交数据库会话,就没法获得新建分类的id。
@app.route('/new-category', methods=['GET', 'POST'])
def new_category():
    name = request.form.get('name')
    category = Category(name=name)
    db.session.add(category)
    db.session.commit()  # commit后使下面的category.id获得实际值
return redirect(url_for('category', id=category.id))
  • SQLALCHEMY_TRACK_MODIFICATIONS:启动项目后会出现一个警告,我不太清楚这个配置的作用(跟踪对象的修改?)。将它设为True可以抑制这个警告。

 

上述配置会在你程序根目录下生成一个data.sqlite文件。而最后实例SQLAlchemy类的db对象表示程序的数据库,你在视图函数里使用它(db)来操作数据库。

 

一对多关系

在这个应用里,用到了一对多关系(条目和分类)。这是条目和分类的模型(Model):

class Item(db.Model):
    __tablename__ = 'items'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))  # step2


class Category(db.Model):
    __tablename__ = 'categories'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))
    items = db.relationship('Item', backref='category')  # step1

实现一个一对多关系需要两步(这里父模型指分类Category,子模型指条目Item):

  1. 在父模型里relationship函数定义一个引向子模型的对象(items)
  2. 在子模型里定义一个外键(category_id),并添加一个到父模型的反向引用(backref)

在存储一个条目到数据库时,同时也要指定一个分类,这里的分类需要是一个对象(Object),而不是id:

body = request.form.get('item')  # 条目的内容
id = request.form.get('category')  # 分类的id
category = Category.query.get_or_404(id)  # 分类的对象
item = Item(body=body, category=category)
db.session.add(item)

这次还用到了另一个扩展——Flask-Script,用来增强命令行功能。暂时没有太深入的应用,有机会再作总结。

 

具体实现

程序比较简单,具体实现没太多需要介绍的。和之前不同的是,这次我使用jQuery对表单进行简单的验证。感兴趣的话,可以去体验一下demo,或是看看源码。为了方便体验(偷懒),没有添加用户系统,你可以随便玩,但条目和分类的数量做了一些限制。

目前功能比较简单,后续会加上拖拽排序,设置日期,设置优先级等功能(考虑加上像wunderlist那样的音效……)。

安装和下载见介绍页:https://helloflask.github.io/todo/

在续篇里,我会介绍使用jQuery(AJAX)把它改造成一个单页应用(SPA,Single-page application)。

 

相关链接

– – – – –

更多关于Flask和Web开发的优质原创内容,欢迎关注Hello, Flask! – 知乎专栏

为你的Flask项目添加富文本编辑器

# 更新(2017/9/30)

可以考虑使用扩展Flask-CKEditor,详情见文章《Flask-CKEditor——为Flask项目集成富文本编辑器》

什么是富文本编辑器?

如果你已经知道什么是富文本编辑器,可以跳到第三段。

简单来说,富文本编辑器就是网页版的Office Word,只不过Word保存.docx格式的文件,它保存HTML代码。之所以叫富文本编辑器(Rich Text Editor),是因为它提供了很多格式工具,比如添加标题,加粗文本等等,所以它又叫做所见即所得编辑器(WYSIWYG – What You See Is What You Get)。与富文本相对的不是穷文本,而是纯文本(Plain text)。比如你在HTML里用input标签生成的输入框,保存的数据就是纯文本。

富文本编辑器ckeditor

富文本编辑器ckeditor

在《Flask Web开发》里,作者为博客添加了一个Markdown编辑器,但是事实上,Markdown并没有得到广泛的支持。基于易用性的考虑,你可能想为你的项目添加一个富文本编辑器。

二选一

对于初学者来说,网上的学习资源太多了,太多的选择首先就是一个问题。你要知道自己需要什么,在不清楚自己需求的情况下,就选一个最常用的。选工具也是这样。基于这些考虑,同类事务,我只推荐一到两个我认为最好的或是最易用的。

  • 不要在入门上花费太多时间;先求深度后求广度。
  • 从需求出发选择工具。

第一个:CKEditor

http://ckeditor.com/

第二个:TinyMCE

https://www.tinymce.com/

这两个编辑器都是开源项目,而且都有丰富的文档。你可以到各自的网站体验一下。

另外,附上这两个编辑器的slogan,你可以做个考量:

  • CKEditor:The Best Web Text Editor for Everyone
  • TinyMCE:The Most Advanced WYSIWYG HTML Editor

选好了吗?那就开始配置吧!二者的配置都相当简单,加上在Flask表单:自定义表单样式里的内容,我们可以很容易的配置成功。

配置CKEditor

 

使用CDN

最方便的方式是使用CDN。

首先在表单类里使用定义一个TextAreaField字段(从wtforms导入):

...
body = TextAreaField(u'正文', validators=[DataRequired(u'内容不能为空!')])
...

然后在要添加编辑器的模板头部添加head块,加入CDN链接:

{% block head %}
{{ super() }}
<script src="//cdn.ckeditor.com/4.5.11/standard/ckeditor.js"></script>
{% endblock %}

如果你使用Flask-Bootstrap,加载其他的JavaScript或是CSS资源时,要使用Jinja2提供的super()函数向父块添加内容,而不是替换父块。更多内容见相关文档

最后在渲染表单时添加一个ckeditor类到这个字段就可以了:

{{ form.body(class="ckeditor") }}

 

在本地加载

下载页面:http://ckeditor.com/download

CKeditor提供了三个版本可供选择:基础、标准和完全,还提供了自定义组件功能。

下载后解压放到你项目的static文件夹,然后在模板引入文件:

{% block head %}
{{ super() }}
<script src="{{ url_for('static', filename='ckeditor/ckeditor.js') }}"></script>
{% endblock %}

其他步骤相同。

需要注意的是,我在把文件直接放在static目录下时加载失败,嵌套了一个文件夹后成功:static/ck/ckeditor/,具体原因未知。

 

配置TinyMCE

使用CDN

首先在表单类里使用定义一个TextAreaField字段:

...
body = TextAreaField(u'正文', validators=[DataRequired(u'内容不能为空!')])
...

使用WTForms或是Flask-Bootstrap渲染表单:

...
{{ form.body() }}  <!-- 使用WTForms -->
...

或是:

...
{{ wtf.form_field(form.body) }} <!-- 使用Flask-Bootstrap -->
...

在模板添加一个head块,然后加载CDN资源,并且初始化。

{% block head %}
{{ super() }}
<script src="//cdn.tinymce.com/4/tinymce.min.js"></script>
<script>tinymce.init({ selector:'textarea' });</script>
{% endblock %}

如果你使用Flask-Bootstrap,加载其他的JavaScript或是CSS资源时,要使用Jinja2提供的super()函数向父块添加内容,而不是替换父块。更多内容见相关文档

 

在本地加载

下载页面:https://www.tinymce.com/download/

下载后解压放到你项目的static文件夹,然后在模板引入文件并初始化:

{% block head %}
{{ super() }}
<script src="{{ url_for('static', filename='tinymce/js/tinymce/tinymce.min.js') }}"></script>
<script>tinymce.init({ selector:'textarea' });</script>
{% endblock %}

其他步骤相同。

如果需要更高级的配置,见各自网站上的文档。

 

富文本的渲染

渲染HTML格式的内容时要使用|safe后缀,这样可以让Jinja2不要转义HTML元素,类似这样:

{{ body|safe }}

关于Jinja2常用的语法和函数的总结,会有一篇文章。

– – – – –

更多关于Flask和Web开发的优质原创内容,欢迎关注Hello, Flask! – 知乎专栏

Flask-Bootstrap:使用本地或其他CDN的资源

Bootstrap的简洁、美观和易用可以让我们在前期不用花费太多的精力和CSS纠缠。

Flask-Bootstrap简化了集成Bootstrap的过程,而且提供了一些方便的工具。我们可以在模板里使用它提供的base.html来创建我们自己的基模板。

在我们的基模板开头引入Flask-Bootstrap提供的base.html,这会帮我们加载所有Bootstrap的资源:

{% extends "bootstrap/base.html" %}

除此之外,它提供的wtf.html模板里有帮助我们快速生成Bootstrap样式的表单函数(比如`quick_form()`)。如果你要考虑到IE9的兼容问题,可以引入它提供的fixes.html。

使用Flask-Bootstrap最常见的问题就是它的CDN问题。它默认使用cdnjs.cloudflare.com的Bootstrap资源,而国内访问速度很慢。

加载本地资源

我们可以通过简单的传入一个配置参数来使用本地的Bootstrap资源:

app = Flask(__name__)
app.config['BOOTSTRAP_SERVE_LOCAL'] = True

使用其他CDN

如果你想使用其他CDN资源,那么可以直接在Flask-Bootstrap的源码里修改,找到\venv\Lib\site-packages\flask_bootstrap\__init__.py,在文件末尾,将下面这些文件的地址修改成你想引用的CDN地址即可:

bootstrap = lwrap(
     WebCDN('//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/%s/' %
                   BOOTSTRAP_VERSION), local)

jquery = lwrap(
     WebCDN('//cdnjs.cloudflare.com/ajax/libs/jquery/%s/' %
                   JQUERY_VERSION), local)

html5shiv = lwrap(
    WebCDN('//cdnjs.cloudflare.com/ajax/libs/html5shiv/%s/' %
                   HTML5SHIV_VERSION))

respondjs = lwrap(
    WebCDN('//cdnjs.cloudflare.com/ajax/libs/respond.js/%s/' %
                   RESPONDJS_VERSION))

比如换成cdn.bootcss.com提供的资源:

bootstrap = lwrap(
    WebCDN('//cdn.bootcss.com/bootstrap/%s/' % BOOTSTRAP_VERSION), local)

jquery = lwrap(
    WebCDN('//cdn.bootcss.com/jquery/%s/' % JQUERY_VERSION), local)

html5shiv = lwrap(
    WebCDN('//cdn.bootcss.com/html5shiv/%s/' % HTML5SHIV_VERSION))

respondjs = lwrap(
    WebCDN('//cdn.bootcss.com/respond.js/%s/' % RESPONDJS_VERSION))

 

参考链接

Flask-Bootstrap源码:https://github.com/mbr/flask-bootstrap

Bootstrap源码:https://github.com/twbs/bootstrap

Flask-Bootstrap文档:http://pythonhosted.org/Flask-Bootstrap/

Flask-Bootstrap中文文档:http://flask-bootstrap-zh.readthedocs.io/zh/latest/

Bootstrap:http://getbootstrap.com/(官网) 或 Bootstrap中文网

(Flask-Bootstrap的中文文档是我业余时间翻译的,如果你发现错误,欢迎到Github上提交修改。项目地址: https://github.com/greyli/flask-bootstrap-docs-zh

更多关于Flask的优质内容,欢迎关注Hello, Flask! – 知乎专栏

Flask表单:自定义表单样式

这篇文章总结了控制表单样式的几种方式和常见的问题。

使用Flask-WTF

在表单类里控制样式

我们可以在表单类里传入一个字典(render_kw),把需要添加到字段的属性以键值对的形式写进去,像这样:

body = TextAreaField(u'正文', validators=[Required(u'内容不能为空!')],         
           render_kw={'class': 'text-body', 'rows': 20, 'placeholder': u'你有什么想法?'})

在HTML里渲染表单:

...
{{ form.body }}
...

渲染的结果:

<textarea class="text-body" id="body" name="body" placeholder="你有什么想法?" rows="20"></textarea>

注意事项:字段名将被作为id的值,这可能会和你定义的其他同名元素冲突。

在渲染时控制样式

我们也可以在渲染时传入字段的属性(不使用render_kw),类似这样:

...
{{ form.body(class_="text-body", rows="20", placeholder="你有什么想法?") }}
...

渲染后的结果和上面一样。

render_kw在WTForms 2.1及以上版本适用。

使用Flask-Bootstrap

使用Flask-Bootstrap提供的表单函数来渲染表单时,也可以像上面那样在表单类或是渲染函数里传入字段的属性:

...
{{ wtf.form_field(form.body, class_="text-body") }}
...

这里需要先导入Flask-Bootstrap提供的表单模板,另外完整的表单形式见Flask表单系列第一篇文章

这里需要注意的是,Flask-Bootstrap会给表单所有字段添加一个form-control来控制样式,这时你再通过render_kw传入已经被定义的属性(class)会失败。如果要传入指定的类,可以在渲染时传入并且增加form-control类

...
{{ wtf.form_field(form.body, class_="form-control text-body") }}
...

或是:

...
{{ form.body(class_="form-control text-body") }}
...

在一些文档里,推荐使用class_=’ ‘的方式来传入类,因为class是Python的保留关键字。

 

小技巧:在输入框里添加图标

有一些网站会在表单左侧放一个图标来增加交互性,比如这样:

表单内的图标

表单内的图标

如果是普通的表单,那么使用Bootstrap提供的表单验证状态类,可以很容易的实现,但是使用Wtforms渲染表单却没法实现。我们可以这样解决:

在CSS里定义一个类,将glyphicon图标的base64数据作为背景图片:

.user-icon {
    padding-left:30px;
    background-repeat: no-repeat;
    background-position-x: 4px;
    background-position-y: 4px;
    background-image:     url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAWCAYAAAArdgcFAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+5pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjY1RTYzOTA2ODZDRjExREJBNkUyRDg4N0NFQUNCNDA3IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIzOUVGMUYxMDY3MTExRTI5OUZEQTZGODg4RDc1ODdCIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIzOUVGMUYwMDY3MTExRTI5OUZEQTZGODg4RDc1ODdCIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgwMTE3NDA3MjA2ODExODA4M0ZFMkJBM0M1RUU2NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowNjgwMTE3NDA3MjA2ODExODA4M0U3NkRBMDNEMDVDMSIvPiA8ZGM6dGl0bGU+IDxyZGY6QWx0PiA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPmdseXBoaWNvbnM8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkX/peQAAACrSURBVHja7JSLCYAwDEQbJ3AER+kouoFu0FEcqSM4gk4QE4ggVRPxg1A8OFCSvkqC5xDRaSZ5ciTjyvzuzbMnwKjY34FHAx618yCQXQHAcVFE5+GoVijgyt3UN1/+hPKFd0a9ubxQa6naMjOdOY2jJAdjZIH7tJ8gzRNuZuho5MriUfpLNbhINXk4Cd27pN3AJVqvQlMPSxSz+oegqXuQhz9bNvDpJfY0CzAA6Ncngv5RALIAAAAASUVORK5CYII=);
}

然后在模板里渲染时传入这个类:

<form method="POST">
    {{ form.hidden_tag() }}
    {{ wtf.form_field(form.username, class_="form-control user-icon") }}
    {{ wtf.form_field(form.password) }}
    {{ wtf.form_field(form.submit) }}
</form>

图标(glyphicon)的base64数据可以在这个网站上可以获取到。

另外,也可以使用一个小图片(25*25大小较合适)来代替base64数据,像这样:

background-image: url({{ url_for('static', filename='user.png') }});
实际效果

实际效果

 

相关链接

  1. http://wtforms.readthedocs.io/en/latest/fields.html#the-field-base-class
  2. http://stackoverflow.com/questions/39520899/flask-wtf-forms-adding-a-glyphicon-in-a-form-field

– – – – –

更多关于Flask和Web开发的优质原创内容,欢迎关注Hello, Flask! – 知乎专栏

Flask表单:表单数据的验证与处理

这篇文章作为上一篇的续篇,所以结构上也和上一篇一样。

使用Flask-WTF

这是一个登录的视图函数:

@auth.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        email = form.email.data
        ...
    return render_template('login.html', form=form)

验证数据

当我们点击了表单上的提交按钮时,form.validate_on_submit()判断会做下面两件事情:

  1. 通过is_submitted()通过判断HTTP方法来确认是否提交了表单
  2. 通过WTForms提供的validate()来验证表单数据(使用我们在下面的表单类里给每个字段传入的验证函数)

这是我们的表单类:

class LoginForm(FlaskForm):
    email = StringField(u'邮箱', validators=[
                DataRequired(message= u'邮箱不能为空'), Length(1, 64),
                Email(message= u'请输入有效的邮箱地址,比如:username@domain.com')])
    password = PasswordField(u'密码', 
                  validators=[Required(message= u'密码不能为空')])
    submit = SubmitField(u'登录')

在Flask-WTF 0.13版本,引入的表单类为FlaskForm
在WTForms 3.0版本,验证函数Required变为DataRequired

当validate()验证未通过时,会在表单字段下面显示我们传进去的错误提示(例如message= u’邮箱不能为空’)。

自定义验证

你可以在表单类创建自定义的验证函数,一个简单的例子:

def validate_username(self, field):
    if User.query.filter_by(username=field.data).first():
        raise ValidationError(u'用户名已被注册,换一个吧。')

这个例子验证用户名是否已经存在(这里使用SQLAlchemy),其中field.data是数据。

ValidationError从wtforms导入,用来向用户显示错误信息,验证函数的名称由validate_fieldname组成。你也可以在这里对用户数据进行预处理:

def validate_website(self, field):
    if field.data[:4] != "http":
        field.data = "http://" + field.data

这个函数对用户输入的网址进行处理(字段名为website)。

你也可以在表单类外面定义一个通用的验证函数,然后传入字段的验证函数列表里,具体见WTForms文档:http://wtforms.readthedocs.io/en/latest/validators.html#custom-validators

获取数据

验证通过后,我们使用form.email.data来获得数据,WTForms提供的静态方法.data返回一个以字段名(field name)和字段值(field value)作为键值对的字典。

 

不使用Flask-WTF,只使用WTForms

我们既然已经知道了Flask-WTF的form.validate_on_submit()的工作原理,我们也可以自己实现:

@auth.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if request.method == 'POST' and form.validate():
        email = form.email.data
        ...
    return render_template('login.html', form=form)

但是这样要复杂一点,而且在渲染时比较繁琐,如果你想了解更多,可以参考:http://flask.pocoo.org/docs/0.11/patterns/wtforms/

 

不使用Flask-WTF,也不使用WTForms

在一些特殊场景下,比如一个特别简单的表单。这是上一个实践项目的表单:

<form method="POST" action="{{ url_for('custom') }}">
    <input type="text" name="time" class="time-input" placeholder="example: 12/30s/20m/2h">
    <input type="submit" class="startButton" value="START">
</form>

这时要给表单添加一个action属性,属性值填写要处理表单数据的视图函数。这是处理表单数据的函数:

@app.route('/custom', methods=['GET', 'POST'])
def custom():
    if request.method == 'POST':
        time = request.form.get('time', 180)
        ...

获取数据

使用request来获取数据(request从flask导入),使用字段的name值来区分,你可以用:

request.form['input-name']

或是:

request.form.get('input-name', default_value)

注意如果你使用第一种方式获取数据,要确保有数据才行,像勾选框这样可以留空的字段,如果用户没有填写数据,提交后会引起HTTP 400错误,这时应该使用第二种方式来设置一个默认值。

你也可以像使用Flask-WTF一样为表单填入已经存储在数据库里的内容,不过要自己填到表单里:

@app.route('/custom', methods=['GET', 'POST'])
def custom():
    if request.method == 'POST':
        time = request.form.get('time', 180)
        ...
    email = 'example@flask.com' 
    return render_template('demo.html', email=email)

在HTML里,将email作为value的值:

<input name="email" value="{{ email }}">

验证数据

因为没有使用WTForms,所以验证数据要自己实现,常见的验证通过你的Python知识大多都很容易实现,错误信息则可以使用flash()传递。

大多数新手对正则表达式比较头疼,在这里推荐一个在线正则表达式编辑网站,regex101.com,支持PHP、PCRE、Python、Golang和 JavaScript,可以写测试,查cheatsheet,生成代码,解释表达式,非常好用!

 

实践:多个表单字段的生成及数据的获取

其实这是后面豆瓣相册实践项目里的内容,就提前透露一点吧。在豆瓣相册里,有一个批量修改的功能,在这个页面,要为你的相册里的每一张图片生成一个编辑框。我们可以直接使用for循环在HTML里生成表单。

生成表单

用for循环生成input,注意我用每个图片的id作为相应输入框的name值:

<form action="{{ url_for('.edit_photos', id=album.id) }}" method="POST">
<ul>
    {% for photo in photos %}
    <li>
        <img class="img-responsive portrait" src="{{ photo.path }}" alt="Some description"/>
        <textarea name="{{ photo.id }}" placeholder="add some description" rows="3">{% if photo.description %}{{ photo.description }}{% endif %}</textarea>
    </li>
    {% endfor %}
</ul>
<hr>
<input class="btn btn-success" type="submit" name="submit" value="submit">

获取数据

然后我在视图函数同样用for循环迭代所有图片,然后使用request获取相应的数据(通过图片的id作为name值获取)然后保存到数据库:

@app.route('/edit-photos/<int:id>', methods=['GET', 'POST'])
@login_required
def edit_photos(id):
    album = Album.query.get_or_404(id)
    photos = album.photos.order_by(Photo.order.asc())
    if request.method == 'POST':
        for photo in photos:
            photo.about = request.form[str(photo.id)]
            db.session.add(photo)
        return redirect(url_for('.album', id=id))
    return render_template('edit_photo.html', album=album, photos=photos)

就先简单讲一下,具体内容等到后面再讲。

相关链接

  1. Flask-WTF文档:Flask-WTF – Flask-WTF 0.13
  2. WTForms文档:WTForms Documentation
  3. Flask文档WTForms章节:http://flask.pocoo.org/docs/0.11/patterns/wtforms/
  4. Flask snippet Form分类:Flask (A Python Microframework)

– – – – –

更多关于Flask的优质内容,欢迎关注Hello, Flask! – 知乎专栏