年度归档:2016年

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

页面右侧出现空白?试试这个CSS调试器!

很多新手在写CSS会遇到很多问题,比如发现页面右侧出现了很多空白。你在网上搜索了之后,发现使用这行代码可以解决:

body, html {
    overflow-x: hidden;
}

但是虽然滚动条被隐藏了,问题并没有解决,你还是不知道是哪个元素在作怪。而且,隐藏了x向的滚动条会导致很多副作用,比如缩小浏览器后无法滚动。

调试工具

最佳的解决方法是使用一个CSS调试工具来把出问题的元素揪出来。你可以访问这个页面,点击页面第二行的链接(Click me……),然后看看发生了什么……不用担心,刷新一下,页面就会恢复正常

如果你使用Chrome浏览器,那么可以试试这个GhostPage扩展。

使用方法

你可以拖拽这个链接到你浏览器的书签栏(或是使用上面提到的Chrome扩展),然后在任一个打开的页面,都可以通过点击这个书签来查看整个页面的元素结构。比如一开始我打开知乎的主页,是这样:

知乎主页

正常的页面

当我点了这个书签,页面就变成了这样:

使用了Ghost CSS后的页面

使用了Ghost CSS后的页面

很神奇吧?

其实原理很简单,只是在页面的CSS文件里加入了这几行代码:

* {
    background: #000 !important;
    color: #0f0 !important;
    outline: solid #f00 1px !important;
}

更改所有元素的背景,颜色和边框(你可以通过修改书签的链接内容来自定义颜色)。

在你的出现右侧空白问题的页面上,你会看到有一个元素一直顶到最右边的边界,修正这个元素就可以彻底的解决问题。这样,一旦页面出现了奇怪的问题时,你就可以通过点击这个书签来检查问题。把它放到你的日常开发工具箱里吧:)

方法来自:Debug Ghost CSS Elements Causing Unwanted Scrolling

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——提问。

翻译Flask-Bootstrap文档

一个月前,开发翻相册的时候,查文档时发现Flask-Bootstrap的文档还没有中文版本,就打算自己来翻译它。花了半天时间了解sphinx的使用方法和reStructuredText的基本语法,注册了Read The Docs账号,然后就开始翻译了。

格式

因为作者使用的是reStructuredText,所以我也去简单了解了一下,一开始是打算自己来创建新的文件,但后来发现有更方便的做法:直接拷贝一份作者的文档源文件,然后直接在文件里翻译,翻译好了再删掉英文。

翻译

几天后发现技术文档的翻译并不简单。虽然平时阅读不少,但很多词汇还是不知道该翻译成什么合适。比如说“serve”这个单词翻译过来是“服务”,但在这下面三个句子里明显不合适。

  1. How do I serve the static files in deployment?

  2. …setting up your server to serve this address.

  3. …to ensure newer Bootstrap versions are served.

接下来我又遇到了“include”,“include”翻译过来是“包含”,常常用来指在HTML的head标签里添加文件资源,比如css文件和网页头像(favicon),但有些时候翻译成“包含”总是感觉怪怪的,缺少一种动词的意味,所以有些地方我把它译成了“加载”。

类似这样的纠结在翻译过程中发生了很多次,还好我手里有一本Flask的书,很多地方可以借鉴。除此之外,还有一些不常见的短语,比如“per default”,这就要靠查资料来解决了,很多时候google翻译也帮不上什么忙。

后记

翻译这份文档大约用了一个月的空闲时间(终于完成了……)。因为是第一次翻译技术文档,有很多地方还需要完善,但也算是收获了翻译的经验,还了解了reStructuredText的语法和文档的部署方法。翻译的过程中还顺便发现了作者的几个小错误,在Github提交了pull request,作者愉快的接受了。

尽管翻译完了,但事情并没有结束,有时间我会再完善一下。如果你发现了错误,欢迎提交改动。

Github项目地址:https://github.com/lihuii/flask-bootstrap-docs-zh

文档地址:http://flask-bootstrap-zh.readthedocs.io/zh/latest/

猪圈里的公民

我的学校有一个很奇怪的事情。

学校的网络由现代技术中心控制,按照程序的设定,每周日到周四晚上十一点断网。但是遇到一些小的假期(比如说中秋节、端午节),这些假期常常是在断网的这几天,这时因为工作人员没有去修改设定,所以仍然会像往常一样断网。

可以想象的是,正在联机打游戏的,看电影看到一半的,正在刷社交动态的人。都会像炸锅了一样疯狂。人一疯狂,就会做出一些平日里不会做的事情,就像很多女生回到宿舍就不再矜持一样。他们或是她们打开窗户大骂,不过也不知道在骂什么,骂路由器?骂网线?骂现代技术中心?骂控制断网的那几行代码?还是骂写那几行代码的人?统统不清楚,他们只是骂,好像上帝听到会把网还给他们一样。到处都是嚎叫和咒骂。

可是这还不过瘾。例行的节目是,勇敢的男同学们开始把自己的或是偷来的暖壶从楼上往下扔,砰!——欢呼——砰!——欢呼——砰!——欢呼。这样几番下来,几栋楼的荷尔蒙逐渐开始安静了。可是还有人不甘心,“再来一个!”,砰!——欢呼,“再来一个!”,“你他妈自己扔呀!”,“……”。终于,一切归于平静。大家都躺倒在床上,举起小屏幕来。虽然屏幕小,网络却不差。

像是一段录好的带子一样,每一个假期都要拿出来放一遍。

这让我想起来猪圈里的猪。猪和主人相处的很融洽,但是有一天,主人恋爱了,经常整天待在外面,很晚才回来。这样一来,就经常忘记给猪喂猪食。猪很生气,于是就在地上拱土,拱出来一个大洞来,然后又去拱门。主人回来很生气,觉得这些猪脑子出问题了,第二天就把它们便宜卖掉了。当然,我可没说扔暖壶的人是猪,毕竟他们那么勇敢和潇洒。

这还让我想起来——张大爷发现邻居赵大爷家的狗经常来他家偷东西吃,就在自己家的院子里转了三圈,以示抗议。

猪不会说话,情有可原。但人能说能走,不管好不好使,总还有个脑子。既然觉得事情不合理,为什么不去找相关部门反映呢。何况这种生活上的事情,完全不会触及到任何敏感点。虽然他们的效率可能会很低,就像我去年报修加投诉,半个月里跑了三趟,才让他们把一个漏水的水龙头修好。但假如扔水壶的人都去跑一趟,我相信问题很快就会解决的。不过反正我不需要,既然没人想去解决,就让故事这样继续下去吧。

那个粗心或是懒惰的程序员,肯定不知道自己竟然带动了整个大学城附近超市的暖壶销量。不过这也顺便害苦了保洁阿姨。

保洁阿姨每次放假回来,都会惊讶的发现,宿舍楼中间的小路上多了好多碎掉的暖壶。时间久了,保洁阿姨也习惯了。在家里的时候,阿姨们很爱跟别人讲这个事情。后来大家都知道了,XX学校的学生上学太压抑太苦了,一放假就兴奋的往楼下扔暖壶……

这个故事告诉我们:写好代码,能救暖壶。

Flask问题集:单个页面两个(多个)表单

出于简化交互的考虑,我们经常见到很多网站把登录页面和注册界面放在同一个页面上,而当我们使用Flask来实现时,却发现问题重重:

  • 不管是哪个表单按下了提交按钮,总是提交第一个表单的数据;
  • 当一个表单数据验证出错时,两个表单都出现了错误提示;

问题的解决

简单来说,问题的主要原因是Flask-WTF的form1.validate_on_submit()并不验证是哪个表单的submit按钮被按下了,只是通过HTTP方法是否是“PUT”或“POST”来判断。

同时form1.submit1.data的data函数会迭代字段的名字(submit)和数据(True/False)作为一个字典,这会产生键重复的问题,所以确保两个表单的submit字段的名字不同。

另外,更改if语句判断条件的顺序(只对按下按钮的表单执行.validate_on_submit()),避免一个表单出错时两个表单同时出现错误提示信息。具体见下面两个代码块。

为你的不同表单里的SubmitField定义不同的名字,像这样:

class Form1(Form):
    name = StringField('name')
    submit1 = SubmitField('submit')

class Form2(Form):
    name = StringField('name')
    submit2 = SubmitField('submit')

....

在 view.py 里添加if判断:

....
form1 = Form1()
form2 = Form2()
....

if form1.submit1.data and form1.validate_on_submit(): # 注意顺序 
    ....
if form2.submit2.data and form2.validate_on_submit(): # 注意顺序
    ....

现在问题解决了,如果你想了解更多一点,那就继续读下去。

探究问题的本质

这是 validate_on_submit():

def validate_on_submit(self):
    """
    Checks if form has been submitted and if so runs validate. This is
    a shortcut, equivalent to ``form.is_submitted() and form.validate()``
    """
    return self.is_submitted() and self.validate()

这是 is_submitted():

def is_submitted(self):
    """
    Checks if form has been submitted. The default case is if the HTTP
    method is **PUT** or **POST**.
    """
    return request and request.method in ("PUT", "POST")

当你对表单调用 form.validate_on_submit() 的时候,它只是通过HTTP方法来检查提交的表单,而不是哪个表单上的按钮被按下了。所以上面的小技巧只是添加一个过滤器(也就是检查按钮是不是有数据,即 form1.submit.data)。

另外,我们改变了if判断的顺序。这样当我们点击其中一个表单上的按钮时,它只对这个表单调用验证函数validate(),所以不会两个表单都出现错误提示。

故事还没完,这是静态方法.data:

@property
def data(self):
    return dict((name, f.data) for name, f in iteritems(self._fields))

它返回一个以字段名(field name)和字段值(field value)作为键值对的字典,但要注意的是,两个表单的提交按钮的字段名都是submit!

这带了什么问题呢?想一想Python里字典的特性就知道了,字典用键来索引一个值,所以会有键重复的问题。

当我们点击第一个表单上的提交按钮(submit)时,form1.submit1.data返回一个像这样的字典:

temp = {'submit': True}

毫无疑问,当我们调用 if form1.submit.data时,它会返回True。

而当我们点击第二个表单上的提交按钮(也是submit)时,if form1.submit.data先生成了一个键值对,然后form2.submit.data生成了键值对,结果这个字典就变成了这样:

temp = {'submit': False, 'submit': True}

这就是为什么我们不论点击那个表单上的按钮,对第一个表单的验证总是返回True。我们通过为两个表单的字段设定不同的名字解决了这个问题。

感谢你读到这里,这样看来,你有很强的好奇心和耐心,而且对编程怀有很大的热情,我说的没错吧:p

首发于Stack Overflow:python – flask-bootstrap with two forms in one page

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

Flask笔记(5):表单

Flask有众多的扩展来简化集成各种常用的库和框架,比如Flask-Bootstrap集成了Bootstrap,Flask-WTF集成了WTForms。这篇要介绍的就是Flask-WTF。

 

安装和配置

安装很简单,使用pip:

pip install 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 CMD):

set SECRET_KEY=very very hard to guess string

后面的字符串不需要用任何符号括起来。

 

表单

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

from flask_wtf import Form # 导入Form
from wtforms import StringField, SubmitField, RadioField, PasswordField, BooleanField # 导入字段
from wtforms.validators import Required, Length, Email # 导入验证函数
class LoginForm(Form):
    email = StringField(u'邮箱', validators=[Required(message= u'邮箱不能为空'), Length(1, 64), Email(message= u'请输入有效的邮箱地址,比如:username@domain.com')])
    password = PasswordField(u'密码', validators=[Required(message= u'密码不能为空')])
    remember_me = BooleanField(u'记住我')
    submit = SubmitField(u'登录')
  • 在验证函数中传入出错时的提示信息,覆盖默认的英文错误提示。
  • WTForms支持的基本字段
  • WTForms支持的验证函数
 

渲染表单

表单有很多种渲染方式。
下面以一个撰写文章的表单为例。

 

默认的渲染方式

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

这种方式渲染出来的表单没有样式,而且很繁琐(当然,你也可以使用for循环来遍历字段)。

 

使用Flask-Bootstrap渲染

Flask-Bootstrap提供了一个模板(wtf.html),可以让你快速生成样式良好的表单。

{% 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) }}
 

控制表单的样式

对于样式,可以通过给表单增加id或class来实现。比如这样:

{{ form.body.label }}{{ form.body(class="post-body") }} # 默认渲染
{{ wtf.form_field(form.body, class="post-body") }} # 使用Flask-Bootstrap渲染

也可以在表单类里传入参数,像这样:

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

在表单字段里传入一个render_kw字典,将HTML的参数和值以键值对的形式写入字典。WTForms2.1及以上版本适用。

这里需要注意的是,Flask-Bootstrap会给表单添加class来控制样式,这时你再通过render_kw传入已经被定义过的参数会失败。所以,如果要使用render_kw传入class,得确保表单字段使用默认的渲染方式。

 

提交表单视图

这是一个添加文章的视图。

@main.route('/new/post', methods=['GET', 'POST'])
@login_required
def new_post():
form = PostForm()
if form.validate_on_submit():
    post = Post(
    title = form.title.data,
    body = form.body.data)
    db.session.add(post)
    db.session.commit()
    return redirect(url_for('.post', id=post.id))
return render_template('edit/new_post.html', form=form)
 

常见问题索引

  1. 一个页面里有多个表单
  2. 文件及多文件上传
  3. 文本编辑器支持

 

Flask笔记索引

这个系列的文章是我学习Flask的经验总结。大致按照《Flask Web开发》这本书的主要章节来分类,总结书里内容的同时增加了其他内容。还有一些是我在开发翻相册的过程中遇到的问题和解决方案。预计在年底完成所有文章。

Flask开发

  1. 环境搭建与项目结构
  2. 程序配置与初始化
  3. 路由和视图函数
  4. 模板
  5. 表单
  6. 数据库
  7. 电子邮件
  8. 测试
  9. 部署

实际应用

  1. 用户认证
  2. 用户角色与权限
  3. Markdown文章
  4. 分页
  5. 关注 
  6. 应用编程接口

问题集

  1. 单个页面多个表单
  2. 文件和多文件上传
  3. 喜欢按钮
  4. 动态删除照片

CSS笔记:Layout

目录

 

一、Display

元素的默认display值是block或inline。

block

block层级的元素总是新起一行,占全宽度。下面的元素默认为block:

<div>
<h1> – <h6>
<p>
<form>
<header>
<footer>
<section>

如果不想让block元素占据全宽度,可以使用width属性设置宽度,同时,如果想让内容适应不同大小的屏幕,使用max-width

inline

inline元素不新起一行,而且只占用需要的宽度。inline元素有:<span>、<a>、<img>

可以重写元素的display默认值来获得特殊效果。

inline-block

inline-block元素和inline元素差不多,但是它可以设置固定的高宽。

none

如果元素的display的属性值为none,那么这个元素将不会显示。<script>元素的display值就是none。这经常被用来控制一个元素的显示的隐藏(使用JavaScript):

document.getElementById("mask").style.display ='none';

display:none or visibility:hidden?

display:none是隐藏一个元素,像是这个元素不存在一样。而visibility:hidden只是使元素不可见,元素仍然占据原有的空间

 

二、Position

有四种值(默认为static):

static 
relative 
fixed 
absolute

static

position属性为static的元素的位置没有特殊效果。

relative

相对定位。这里的relative是指相对于正常的位置进行定位。可以设置top,right,bottom和left来偏移位置。

fixed

固定定位。相对于浏览器窗口来说是固定的,无论你怎么缩放窗口大小,或是滚动内容。可以设置top,right,bottom和left来偏移位置。

absolute

绝对定位。相对于最近的父元素(position值为static的元素除外)进行定位。可以设置top,right,bottom和left来偏移位置。

z-index

z-index属性用来指定一个元素在z轴上的位置,利用这个属性可以实现元素的重叠。如果不指定,在HTML文件里,代码位置靠后的元素会在上面。主要使用场景有: 1. 在图片上添加一个按钮,这时把按钮的z-index设置比图片大,就可以点击到按钮而不是图片。 2. 在图片上显示文字,只要把图片的z-index设为-1就可以了。

 

三、Overflow

Overflow用来定义当元素的内容超过限定的范围后的显示方式。有四种值:

visible

默认值。超出的内容仍然显示。

hidden

超出的内容将被隐藏。

scroll

添加一个滑动条。(将同时添加水平方向和竖直方向的滚动条,即使不需要。)

auto

类似于scroll,不过只在需要的时候添加滑动条。

overflow-x overflow-y

这两个值用来分别定义两个方向的overflow值。

div { 
    overflow-x: hidden; /* 隐藏超出内容 */ 
    overflow-y: scroll; /* 添加垂直滑动条 */ 
}

 

四、Float

float用来设置元素的滚动,像这样float: left;。一般的用法有:让文字包裹图片;制作navbar,让li元素向左浮动。

浮动的元素下面的元素会包裹住浮动的元素,这时可以用clear用来控制元素的滚动,下面这行让左边的元素不滚动:

div { 
    clear: left; 
}

 

五、Align

  • 水平居中:使用margin: auto;(在IE8里需要声明!DOCTYPE)。这时要给元素限定宽度,比如width: 50%;,限定的宽度之外的空间会被一分为二。
  • 垂直居中:使用padding属性可以让元素在父容器里垂直居中padding: 50px 0;
  • 文本居中:text-align: center;
  • 图片居中:

display: block; 
margin: auto;
  • 居左或右:使用float。

  • 水平+垂直居中:使用padding加上text-align: center;。或是使用position和transform:

.center { 
    height: 200px; 
    position: relative; 
    border: 3px solid green; 
} 

.center p { 
    margin: 0; 
    position: absolute; 
    top: 50%; 
    left: 50%; 
    transform: translate(-50%, -50%); 
}