标签归档:Flask

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! – 知乎专栏

Flask表单:表单的创建与渲染

使用Flask-WTF创建和渲染表单

Flask-WTF是一个集成了WTForms的Flask扩展,使用它你可以在python文件里创建表单类,然后在HTML使用它提供的函数渲染表单。

Flask-WTF

Flask-WTF

跨站请求伪造保护

Flask-WTF默认支持CSRF(跨站请求伪造)保护,只需要在程序中设置一个密钥。Flask-WTF使用这个密钥生成加密令牌,再用令牌验证表单中数据的真伪。(关于Flask项目的配置和初始化,后续文章会谈到)你可以使用app.config字典来存储配置变量:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

或是从系统环境变量中获取

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

很多重要的信息,比如邮箱账户和密码都不能直接写在程序里,而要设置系统环境变量,设置方法如下(Windows):

set SECRET_KEY=very very hard to guess string

*Unix用户使用export替换set。
注意,后面的字符串不需要用任何符号括起来。

创建表单

然后我们就可以开始创建表单了,以一个简单的登录表单为例,首先是导入:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField
from wtforms.validators import DataRequired, Length, Email

从Flask-WTF导入Form类,而表单字段和验证函数直接从WTForms导入。

每个表单都用一个继承自Form的类表示,每个字段都用一个对象表示,每个对象可以附加多个验证函数。常见的验证函数有Required(),Length(),Email()等。

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

然后是我们的表单类:

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'登录')

在验证函数中传入出错时的提示信息,覆盖默认的英文错误提示。

参考链接:

表单渲染

首先需要在视图函数里引入一个表单类的实例,然后在返回模板的时候,传入这个实例:

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

在HTML文件(login.html)里,我们使用下面的方式来渲染表单:

<form class="form" method="POST">
    {{ form.hidden_tag() }}
    {{ form.email.label }}{{ form.email() }}
    {{ form.password.label }}{{ form.password() }}
    {{ form.submit() }}
</form>

这种方式渲染出来的表单没有样式(字段后面的括号里可以填入input属性,如果不填可以省略掉括号,控制表单样式内容见续篇《自定义表单样式》),而且比较繁琐,你也可以使用for循环来遍历字段。

关于form.hidden_tag()

上面说了,Flask-WTF支持跨站请求伪造保护,所以它会为你的表单类自动创建一个CSRF字段,你需要在表单里渲染这个字段:{{ form.csrf_token }}。

而如果你有多个隐藏字段(type属性为hidden)的话,就可以使用{{ form.hidden_tag() }}来渲染所有隐藏字段。

 

使用Flask-Bootstrap渲染表单

Flask-Bootstrap是一个集成了Bootstrap的Flask扩展,它提供了一些表单渲染的函数,可以让我们方便的渲染Bootstrap样式的表单。

要使用这些函数,首先你要安装Flask-WTF,然后在HTML文件开头引入提供这些函数的模板文件(wtf.html):

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

使用Flask-Bootstrap提供的wtf.form_field()函数来渲染表单:

{% import "bootstrap/wtf.html" as wtf %}
<form class="form" method="POST">
    {{ form.hidden_tag() }}
    {{ wtf.form_field(form.title) }}
    {{ wtf.form_field(form.body) }}
    {{ wtf.form_field(form.submit) }}
</form>

假如你不需要调整Boostrap的默认表单样式,只想要快速生成表单,那么Flask-Bootstrap还提供了一个强大的函数,只需要一行就可以快速生成表单:

{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}

 

纯HTML表单或混合

你可以把你想定义的字段与Flask-WTF的渲染字段混在一起,比如像这样直接将确认按钮写在模板里:

<form method="POST">
    {{ form.csrf_token }}
    {{ form.name.label }} {{ form.name(size=20) }}
    <input type="submit" value="确认">
</form>

注:在视图函数里获取数据时,Flask-WTF不验证是否按下了确认按钮,只是判断HTTP方法。

你也可以不使用Flask-WTF,直接把表单写在HTML模板里。这在一些特殊情况下,反而会比使用Flask-WTF更加方便。比如在小项目里,或是需要高度自定义的情况下。在上一个Flask实践项目计时器里,传入时间参数的表单就是直接写在HTML里的:

<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属性,属性值填写要处理表单数据的视图函数。下一篇文章会详细谈到怎么处理表单的数据。

Flask扩展一般通过安装pip安装(注意要安装在虚拟环境,别忘了激活虚拟环境后再安装):

pip install flask-wtf

注意导入时名称的变化,《Flask Web开发》中使用flask.ext.extension_name导入扩展的方式被废弃,使用下面的方式(flask_extension_name)导入扩展中的函数或类:

from flask_wtf import FlaskForm

*考虑写一篇文章讲一下扩展的安装、初始化和配置

 

参考链接

  1. Cross-site request forgery
  2. Flask-WTF – Flask-WTF 0.13
  3. WTForms Documentation
  4. http://flask.pocoo.org/docs/0.11/patterns/wtforms/
  5. http://flask.pocoo.org/snippets/category/forms/

– – – – –

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

Flask模板引擎:Jinja2语法介绍

Jinja是组成Flask的模板引擎。可能你还不太了解它是干嘛的,但你对下面这些百分号和大括号肯定不陌生:

{% block body %}
  <ul>
  {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.username }}</a></li>
  {% endfor %}
  </ul>
{% endblock %}

看过《Flask Web开发》,很多人都能写出来这些,但除了书里讲的,你还应该了解一些其他的语法细节。这篇文章就来介绍一些常用的语法和函数,如果想要系统完整的了解Jinja,可以去读它的文档:http://jinja.pocoo.org/docs/dev/

FAQ

在Jinja网站上的FAQ里,我挑了三个大家可能会比较感兴趣的问题(简单翻译了一下)。

1、为什么要叫Jinja?

之所以叫Jinja,是因为日本的神社(Jinja)英文单词是temple,而模板的英文是template,两者发音很相似(这么说来,它本来也有可能叫Miao的……)。

2、Jinja的速度怎么样?

和Mako差不多,但比Genshi以及Django的模板引擎快10~20倍。

3、把逻辑判断(Logic)放到模板里是个好主意吗?

毫无疑问,你放到模板里逻辑判断(Logic)应该越少越好。但为了让大家都开心,适当的逻辑判断是需要的。尽管如此,它有很多对于你能做什么,不能做什么的限制。

出于诸多考虑(速度,易读性等等),Jinja既不允许你放置任意的Python代码,也不允许所有的Python表达式。这也是为什么我们要了解Jinja2的语法。

 

Delimiters(分隔符)

多说一下注释,这是单行注释:

{#% for user in users %#}

下面是多行注释:

{# note: commented-out template because we no longer use this
    {% for user in users %}
        ...
    {% endfor %}
#}

 

Variables(变量)

除了普通的字符串变量,Jinja2还支持列表、字典和对象,你可以这样获取变量值:

{{ mydict['key'] }}
{{ mylist[3] }}
{{ mylist[myintvar] }}
{{ myobj.somemethod() }}

获取一个变量的属性有两种方式:

{{ foo.bar }}
{{ foo['bar'] }}

这两种方法基本相同(深层次的区别可以暂不考虑)

Filter(过滤器)

过滤器用来修改变量,使用一个竖线和变量相隔。

{{ items|join(', ') }}

常用的内置过滤器:

  • safe 渲染时不转义
  • capitalize 首字母大写
  • lower 小写
  • upper 大写
  • title 每个单词的首字母都转换成大写
  • trim 去掉首尾空格
  • striptags 去掉值里的HTML标签
  • default 设置一个默认值,如果变量未定义,就用这个默认值替换。类似这样:
{{ my_variable|default('my_variable is not defined') }}
  • random(seq) 返回一个序列里的随机元素
  • truncate(s, length=255, killwords=False, end=’…’) 截取出指定长度的文章(文章摘要)
  • format(value, *args, **kwargs) 参考Python的字符串格式化函数
  • ……

完整的fliter列表:http://jinja.pocoo.org/docs/dev/templates/#builtin-filters

Tests(测试,判断)

Jinja2提供的tests可以用来在语句里对变量或表达式进行测试,如果要测试一个变量,可以在变量后加上“is”和test名,比如:

{% if user.age is equalto 42 %} {# 这里也可以写成... is equalto(42) #}
    Ha, you are 42!
{% endif %}

如果要传入参数,可以在test后增加括号,也可以直接写在后面

常用的test(未说明的均返回True或False):

  • defined
  • equalto
  • escaped
  • none
  • sequence
  • string
  • number
  • reverse
  • replace
  • ……

完整的test列表及用法见:http://jinja.pocoo.org/docs/dev/templates/#list-of-builtin-tests

Loop(循环)

在一个for循环内,有一些特殊的变量可以使用,这是几个常用的:

  • loop.index 当前迭代数,可以用来写评论的楼层数(从1开始)
  • loop.index0 同上,不过从0开始迭代
  • loop.revindex 反向的迭代数(基数为1)
  • loop.revindex0 反向的迭代数(基数为0)
  • loop.length 序列的数量
  • loop.first 是否是第一个元素
  • loop.last 是否是最后一个元素
  • ……

完整的列表见:http://jinja.pocoo.org/docs/dev/templates/#for

 

Whitespace Control(空格控制)

默认的设置:

  1. 如果末尾有换行符,则去除;
  2. 其他空格原样保留。

也就是说,下面这几行:

<div>
    {% if True %}
        yay
    {% endif %}
</div>

渲染后的结果是这样:

<div>

        yay

</div>

Jinja2语句占据的空行,你自己输出的空格,Tab都将保留。

如果要去掉Jinja2语句占据的空行,可以通过设置Jinja2的环境变量实现:

app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True

或者像这样手动添加一个减号(注意和%之间没有空格):

<div>
    {% if True -%}
        yay
    {%- endif %}
</div>

两者实现的效果相同,如下:

<div>

        yay

</div>

如果语句块的前后都加上减号:

<div>
    {%- if True -%}
        yay
    {%- endif -%}
</div>

渲染后会是这样:

<div>yay</div>

通过Jinja2提供的环境变量,你可以设置很多东西,比如分隔符(在和其他的语言产生冲突时,可以通过修改分隔符来解决)。具体见:http://jinja.pocoo.org/docs/dev/api/#jinja2.Environment

 

Escaping(转义)

有时你会想原样输出一些Jinja2语句和分隔符,对于小的内容,可以使用变量表达式来输出,比如输出一个分隔符:

{{ '{{' }}

大的内容块可以使用一个raw块包裹:

{% raw %}
    <ul>
    {% for item in seq %}
        <li>{{ item }}</li>
    {% endfor %}
    </ul>
{% endraw %}

 

模板继承

你可以创建一个base.html作为基模板,把导航栏、页脚、flash消息、js或css文件等等需要在每一个页面中显示的内容放在基模板里,然后在其他的模板(子模板)里使用这个语句继承它:

{% extends "base.html" %}

如果想添加内容到在父模板内已经定义的块,可以使用super函数:

{% block sidebar %}
    <h3>Table Of Contents</h3>
    ...
    {{ super() }}
{% endblock %}

这样可以避免覆盖父块的内容。

全局函数

常用的全局函数有:

  • range([start, ]stop[, step])
  • lipsum(n=5, html=True, min=20, max=100) 为模板生成一些 lorem ipsum。

详细列表见:http://jinja.pocoo.org/docs/dev/templates/#list-of-global-functions

 

其他内容

内容还有很多,比如行语句、控制流、表达式、宏等。不再一一介绍了(写这种介绍文章太累了……)。

具体见文档的模板部分:

Template Designer Documentation

相关链接

Jinja主页:http://jinja.pocoo.org

Jinja2文档:http://jinja.pocoo.org/docs/dev/

Jinja2文档模板部分:http://jinja.pocoo.org/docs/dev/templates/

Github项目页:https://github.com/pallets/jinja

– – – – –

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

Flask实践:计时器

Demo体验:计时器 – Hello, Flask!
难度:1
涉及知识点:URL变量

计时界面

计时界面

– – – – –

我们经常在一些网站上看到倒计时,比如购物网站上的秒杀倒计时,或是考试网站上的距离高考还剩多少天……

我们今天就用Flask和JavaScript(jQuery)来实现一个在线计时器,具体的User Strory:

  • 可以在首页点击不同的时间按钮进入计时
  • 计时结束后会有弹窗和铃声提示
  • 可以在输入框里输入参数进入相应的计时,比如“34、23s、20m、2h”
  • 可以通过在url里传入时间来开始计时,比如:

项目结构

|-Timer-Flask 项目名称
    |-app.py
    |-templates/  模板文件夹
        |-index.html  
    |-static/
        |-beep.mp3  计时结束铃声
        |-favicon.ico  站点图标
        |-style.css
        |-js/
            |-progressbar.js
            |-jquery.min.js
            
    |-venv/  虚拟环境

实现代码

主程序:app.py

import re
from flask import Flask, render_template, url_for, redirect, request, flash

app = Flask(__name__)
app.config['SECRET_KEY'] = 'a very secret string'


@app.route('/')
def index():
    return redirect(url_for('timer', num=11*60+11))


@app.route('/<int:num>s')
@app.route('/<int:num>')
def timer(num):
    return render_template('index.html', num=num)


@app.route('/custom', methods=['GET', 'POST'])
def custom():
    time = request.form.get('time', 180)
    # 使用正则表达式来验证输入的字符
    m = re.match('\d+[smh]?$', time)
    if m is None:
        flash(u'请输入一个有效的时间,例如34、20s、15m、2h')
        return redirect(url_for('index'))
    if time[-1] not in 'smh':
        return redirect(url_for('timer', num=int(time)))
    else:
        type = {'s': 'timer', 'm': 'minutes', 'h': 'hours'}
        return redirect(url_for(type[time[-1]], num=int(time[:-1])))


@app.route('/<int:num>m')
def minutes(num):
    return redirect(url_for('timer', num=num*60))


@app.route('/<int:num>h')
def hours(num):
    return redirect(url_for('timer', num=num*3600))


@app.errorhandler(404)
def page_not_fouond(e):
    flash(u'访问地址出错了,鼠标放在问号上了解更多: )')
    return redirect(url_for('timer', num=244))

计时的功能主要用JavaScript(jQuery)实现,在index.html,传递变量给JavaScript:

{% block scripts %}
<script>
    var Minutes = {{ num }};
</script>
{% endblock %}

另外,在这个APP里,因为表单很小,所以没有使用Flask-WTF。表单部分:

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

然后在视图函数里,我使用request来获取数据,使用正则表达式验证数据:

@app.route('/custom', methods=['GET', 'POST'])
def custom():
    # 设置180为默认值,避免提交空白表单产生400错误
    time = request.form.get('time', 180) 
    # 使用正则表达式验证数据
    m = re.match('\d+[smh]?$', time)
    if m is None:
        flash(u'请输入一个有效的时间,例如34、20s、15m、2h')
        return redirect(url_for('index'))
    if time[-1] not in 'smh':
        return redirect(url_for('timer', num=int(time)))
    else:
        type = {'s': 'timer', 'm': 'minutes', 'h': 'hours'}
        return redirect(url_for(type[time[-1]], num=int(time[:-1])))

下一次会谈一下表单的几种不同的验证方式和一些处理技巧。

完整的实现见源码(链接在底部),欢迎fork和patch(或是star:)。

相关知识点

  • URL变量

大部分现代的网站(app)都会有一个美观简洁的URL,比如http://www.example.com/user/kitty。在Flask里,我们通过在URL里设置变量来实现这个功能,比如说上面的URL,对应的路由就是:

app.route('/user/<username>')

这个<variable_name>可以作为参数传递到视图函数里,我们还可以使用Flask提供的转换器,以<converter:variable_name>的形式来转换变量:

@app.route('/user/<username>')
def show_user_profile(username):
    # show the user profile for that user
    return 'User %s' % username

@app.route('/post/<int:post_id>')
def show_post(post_id):
    # show the post with the given id, the id is an integer
    return 'Post %d' % post_id

下面是Flask支持的转换器:

string accepts any text without a slash (the default)
int accepts integers
float like int but for floating point values
path like the default but also accepts slashes
any matches one of the items provided
uuid accepts UUID strings

使用这个特性,计时器实现了在地址后填入参数就可以进入相应的计时。

相关链接

DEMO:http://timertimer.herokuapp.com/

源码:https://github.com/helloflask/timer-flask

– – – – –

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

Flask实践:猜数字游戏

Demo体验:猜数字 – Flask
难度:1
使用扩展:Flask-Bootstrap、Flask-WTF

guess1

 

本文首发于知乎专栏:Hello, Flask!

– – – – –

每个学编程的人大概都写过猜数字游戏,今天我们用Flask来做一个Web版本的猜数字。功能很简单,只有两个路由,三个模板和一个表单组成。扩展的版本见项目的Github页面(页尾)。

项目结构

|-GuesstheNumber 项目名称
    |-guess.py
    |-templates/  模板文件夹
        |-index.html  
        |-guess.html
        |-base.html  基模板
    |-venv/  虚拟环境

实现代码

guess.py

# -*- coding: utf-8 -*-
import random

from flask import Flask, render_template, flash, redirect, url_for, session
from flask_wtf import Form
from wtforms import IntegerField, SubmitField
from wtforms.validators import Required, NumberRange
from flask_bootstrap import Bootstrap

app = Flask(__name__)
app.config['SECRET_KEY'] = 'very hard to guess string'  #设置secret key
bootstrap = Bootstrap(app)  # 初始化Flask-Bootstap扩展


@app.route('/')
def index():
    # 生成一个0~1000的随机数,存储到session变量里。
    session['number'] = random.randint(0, 1000)  
    session['times'] = 10
    return render_template('index.html')


@app.route('/guess', methods=['GET', 'POST'])
def guess():
    times = session['times'] # 从session变量里获取次数
    # 从session变量里获取在index函数里生成的随机数字
    result = session.get('number')  
    form = GuessNumberForm()
    if form.validate_on_submit():
        session['times'] = times  # 更新次数值
        if times == 0:
            flash(u'你输啦……o(>﹏<)o')
            return redirect(url_for('.index'))
        answer = form.number.data
        if answer > result:
            flash(u'太大了!你还剩下%s次机会' % times)
        elif answer < result:
            flash(u'太小了!你还剩下%s次机会' % times)
        else:
            flash(u'啊哈,你赢了!V(^-^)V')
            return redirect(url_for('.index'))
    return render_template('guess.html', form=form)


class GuessNumberForm(Form):
    number = IntegerField(u'输入数字(0~1000):', validators=[
        # 传入验证函数和相应的错误提示信息。
        Required(u'输入一个有效的数字!'),
        NumberRange(0, 1000, u'请输入0~1000以内的数字!')])
    submit = SubmitField(u'提交')


if __name__ == '__main__':  # 用于heroku部署,本地可省略
    app.run()

index.html

{% extends "base.html" %}

{% block page_content %}
<!-- 传入url_for的参数是视图函数的名称 -->
<a class="btn btn-success btn-lg" href="{{ url_for('guess') }}">开始游戏</a>
{% endblock %}

guess.html

{% extends "base.html" %} <!-- 引入基模板 -->
{% import "bootstrap/wtf.html" as wtf %}

{% block page_content %}
    <!-- 使用Flask-Bootstrap提供的函数来生成默认样式的表单 -->
    {{ wtf.quick_form(form) }}
{% endblock %}

完整的项目见源码(底部)。

相关知识

  • session(会话)

session是Flask的上下文(context)全局变量,可以用来存储(用字典的形式)请求之间需要“记住”的值。在这个猜数字游戏里,我使用它来存储生成的随机数和剩余的机会次数。

要使用session,得先设置一个secret key,这用来给Cookie签名以加密session,这样做的效果是用户可以看到cookie但不能篡改它。尽管如此,session并不是安全的,不能用来存储密码,这个视频演示了一个破解session的过程:https://youtu.be/mhcnBTDLxCI

更多细节见:http://flask.pocoo.org/docs/0.11/quickstart/#sessions

安装和运行

源码地址:https://github.com/helloflask/guess-flask

下载或使用git命令克隆项目后,切换到程序根目录。使用virtualenv创建一个虚拟环境,激活后使用pip安装所需依赖:

pip install -r requirements.txt

然后运行:

set FLASK_APP=guess.py    
flask run

访问: http://127.0.0.1:5000/

上面的代码稍微美化一下,变成了下面这样:
guess2

源码:https://github.com/helloflask/guess

Demo:http://guessguess.herokuapp.com/

我的知乎专栏

注册知乎以来,一直都在潜水,关注一些有趣和有价值的东西,收获了不少帮助。最近写关于Flask的文章,觉得写在博客里太零散,于是就开了个专栏。

开通这个专栏,一来是关于Flask的文章放在专栏里更加系统化,不会和其他的文章混在一起。二来也算是对这个社区的一点小小的贡献,作为对那些无偿分享知识的人的感谢和响应。

这个专栏主要用来写我学习和使用Flask过程中的经验和总结,还有我用Flask做的一些项目的具体实现。主要的写作主题:

  • Flask介绍
  • Flask笔记 —— 包含Flask开发涉及的常用主题,比如模板、表单等。
  • Flask问题集 —— Flask开发中常见的问题,比如在一个页面多个表单的问题。
  • Flask常用插件介绍 —— 介绍常用Flask插件和基本用法。
  • 实践:用Flask写个人博客
  • 实践:用Flask从零实现豆瓣相册
  • 其他相关的前端框架和库的介绍

欢迎关注:Hello, Flask! ——知乎专栏

同时创建了一个Github Organization,专栏上的代码会放在这里:https://github.com/helloflask

Flask问题集:flash消息分类与美化

bootstrap alert style

Bootstrap提供的alert样式,依次为:‘success’、‘info’、‘warning’、‘danger’。

 

Flask提供了一个很方便的flash函数,在视图函数里只需要一行就可以在网页上闪现一个消息(alert),像这样:

flash(u'登录成功,欢迎回来!')

我们今天来为它添加样式。

 

基本用法

在视图函数里使用flash:

flash(u'要闪现的消息内容')

然后在基模板里使用`get_flashed_messages()`来获取消息,并渲染出来。
比如《Flask Web开发》里使用alert-warning渲染所有的消息:

{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{ message }}
</div>
{% endfor %}

下面我们使用Bootsrtap的消息样式来渲染不同的消息。

 

使用Bootstrap的消息(alert)样式

如果想要开启消息的分类,需要在调用`get_flashed_messages()`时传入参数`with_categories=True`。这时,在基模板里使用这个函数获取消息时,得到的是一个由`(category, message)`形式的元组组成的一个列表。

之后,就可以在使用flash函数时加入消息类别:

flash(u'登录成功,欢迎回来!', 'info')

你可以使用‘success’、‘info’、‘warning’、‘danger’这四个值中的任一个。具体样式文章开头的图片。

这时基模板里渲染消息的部分要改成这样:

{% for message in get_flashed_messages(with_categories=True) %}
<div class="alert alert-{{ message[0] }}">
    <button type="button" class="close" data-dismiss="alert">&times;</button>
    {{ message[1] }}
</div>
{% endfor %}

这样一来,每个消息就会按照我们提供的分类样式来渲染了。

 

DIY

当然,你也可以自己定义每一类消息的样式。我推荐你在Bootstrap的四个类别的基础上增加样式,这样只需要在你的css文件里添加一个新的alert样式,比如下面我添加了一个code样式:

/* code alert style, by Grey Li*/
.alert-code {
    color: #66ff66;
    background-color: #000000;
    border-color: #ebccd1;
}

.alert-code hr {
    border-top-color: #000066;
}

.alert-code .alert-link {
    color: #ff9900;
}

然后使用:

flash(u'要闪现的消息', 'code')

效果:

我设计的alert样式

我设计的alert样式:p

如果你不用Bootstrap,只需要加将上面的为div指定类的那行改成:

<div class="{{ message[0] }}">

然后在你的css文件里定义类别对应的样式就可以了。

 

多看源码

源码里面有很多有趣和有价值的东西,要养成看源码的习惯。很多问题靠看源码里的注释基本上就能解决。所以,问题的正确解决方式应该是:看源码——Google——提问。