日課書

编程100小时挑战

博客分页功能

当博客有很多文章后,在首页显示所有文章,会让加载速度变慢,同时也会影响浏览体验。一个好的办法,就是对博文进行分页,每页仅显示一部分文章,然后通过导航跳转到相应的页面。如果使用Flask,可以利用SQLAlchemy和Bootstrap插件的分页功能,高效的实现博客分页效果。

0. 创建虚拟文章数据

想要实现博客的分页功能,首先需要我们的博客有足够的数据,在开发阶段,只能通过自动产生数据的手段来满足要求。Python中的ForgeryPy包,可用于产生虚拟信息。

首先安装forgerypy,pip install forgerypy

为博客的文章Post模型,创建类方法,用来产生虚拟数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
summary = db.Column(db.String)
content = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))

@staticmethod
def generate_fake(count=100):
from random import seed, randint
import forgery_py

seed()
for i in range(count):
p = Post(title=forgery_py.lorem_ipsum.title(),
summary=forgery_py.lorem_ipsum.sentences(randint(1,2)),
content=forgery_py.lorem_ipsum.sentences(randint(5,8)),
timestamp=forgery_py.date.date(True),
author_id=User.query.filter_by(is_administrator=True).first().id
)
db.session.add(p)
db.session.commit()

1. 在页面中获得分页渲染数据

为了支持分页,我们在视图函数中需要获取到,分页类型的数据。可以利用SQLAlchemy提供的paginate()方法,,来显示某页记录。同时可以使用get方法来获取,通过查询字符串方式添加到URL的页码。

1
2
3
4
5
6
def index():
# ...
page = request.args.get('page', 1, type=int)
pagination = Post.query.order_by(Post.timestamp.desc()).paginate(page, per_page=current_app.config['GRITY_POSTS_PER_PAGE'], error_out=False)
posts = pagination.items
return render_template('index.html', posts=posts, pagination=pagination)

2. 添加分页导航

paginate()方法返回值是一个Pagination类对象,这个对象在SQLAlchemy中定义。其中很多属性可以用于在模板中生成分页链接,因此将其作为参数传入模板。在模板中利用Bootstrap的分页css类就能实现分页导航。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- app/templates/_macros.html -->
{% macro pagination_widget(pagination, endpoint) %}
<ul class="pagination">
<li{% if not pagination.has_prev %} class="disable"{% endif %}>
<a href="{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.page-1,
**kwargs) }}{% else %}#{% endif %}">
&laquo;</a>

</li>
{% for p in pagination.iter_pages() %}
{% if p %}
{% if p == pagination.page %}
<li class="active">
{% else %}
<li>
{% endif %}
<a href="{{ url_for(endpoint, page=p, **kwargs) }}">{{ p }}</a>
</li>
{% else %}
<li class="disable"><a href="#">&hellip;</a></li>
{% endif %}
{% endfor %}
<li{% if not pagination.has_next %} class="disable"{% endif %}>
<a href="{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.page+1,
**kwargs) }}{% else %}#{% endif %}">
&rquo;</a>

</li>
</ul>
{% endmacro %}

把创建的宏引入页面

1
2
3
4
5
6
...
{% import "_macros.html" as macros %}
...
<div class="pagination">
{{ macros.pagination_widget(pagination,'.index') }}
</div>

在博客中支持Markdown

Markdown是一种轻量级的「标记语言」,它允许作者使用简单的标签控制文章的版式,同时不降低作为纯文本的可读性。而且利用各种方式可以很方便的把它转换成排版精美的HTML页面。所以Markdown非常适合写博客。

在博客中我们希望,输入文章的文本输入支持Markdown语法,同时要有实时预览的功能。如果是上传的Markdwon文本,也能有转换成相应的HTML。

0. 用到的功能包

  • PageDown:用JavaScript实现的客户端Markdown到HTML转换程序。
  • Flask-PageDown:为Flask包装的PageDown,把PageDown集成到Flask-WTF表单中。
  • Markdown: 使用Python实现的服务器端Markdown到HTML的转换程序。
  • Bleach:使用Python实现的HTML过滤器。

安装:$ pip install flask-pagedown markdown bleach

1. 使用Flask-PageDown

初始化Flask-PageDown

1
2
3
4
5
6
7
8
from flask_pagedown import PageDown
# ...
pagedown = PageDown()
# ...
def create_app(config_name):
# ...
pagedown.init_app(app)
# ...

把WTF表单中的多行文本控件,修改为PageDownField

1
2
3
4
5
from flask_pagedown.fields import PageDownField

class PostForm(Form):
content = PageDownField("What's on your mind?", validators=[Required()])
# ...

加载Javas脚本

1
2
3
4
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}

2. 在服务器上处理Markdown

在表单提交后,之前的预览格式就会消失,上传到服务器的只有Markdown文本。我们可以在需要显示文章时,在页面渲染Markdown,但如果每次都这么做,效率很低。因此我们在服务器端把Markdown转换成HTML代码,使用Bleach留下需要的标签,保存到数据库中。当需要显示文章时,就直接读取相应的HTML代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from markdown import markdown
import bleach

class Post(db.Model):
# ...
content_html = db.Column(db.Text)

# ...
@staticmethod
def on_changed_content(target, value, oldvalue, initiator):
allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em',
'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', 'h2', 'h3', 'h4', 'p']
target.content_html = bleach.linify(bleach.clean(markdown(value,
output_format='html'), tags=allowed_tags, strip=True))

db.event.listen(Post.content, 'set', Post.on_changed_content)

在模板中使用文章美容的HTML格式

1
2
3
4
5
6
7
<div class="post-content">
{% if post.content_html %}
{{ post.content_html | safe }}
{% else %}
{{ post.content }}
{% endif %}
<div>

3. 博客文章固定连接

每篇博客文章都应该有一个专门的页面,固定的链接,方便在网上分享传播。这个URL可以是文章ID或者更有意义、可读性高的字符串。

1
2
3
4
@main.route('/post/<int:id>')
def post(id):
post = Post.query.get_or_404(id)
return render_template('post.html', posts=[post])

在html模板中可以使用如下方法,为博客文章指定固定链接

1
<a href="{{ url_for('.post', id=post.id) }}">Permalink</a>

4. 博客文章编辑器

博客登录后,管理员可以对文章进行编辑。编辑文章需要单独的页面。这个页面会有表单显示原来文章的相关信息,可以在这上面直接修改,同时下面会有文章预览,提交后修改完成。

编辑博客文章的路由如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@admin_required
@main.route('/edit/<int:id>', methods=['GET', 'POST'])
def edit(id):
post = Post.query.get_or_404(id)
form = PostForm()
if form.validate_on_submit():
post.title = form.title.data
post.summary = form.summary.data
post.content = form.content.data
db.session.add(post)
flash('The post has been updated.')
return redirect(url_for('.post', id=post.id))
form.title.data = post.title
form.summary.data = post.summary
form.content.data = post.content
return render_template('edit_post.html', form=form)

编辑博客文章的模板和创建文章基本相同。之后再需要进入编辑的地方添加如下链接

1
2
<a href="{{ url_for('.edit', id=post.id) }}"><span class="label label-danger">
Edit</span></a>

100小时编程挑战——半程感想

十多天前,重看了Meditic的一篇文章,野兽派游戏,颇有感触。大意是,人从本质上看,是一种野兽,其实不需要太多的东西,能维持温饱,生存下去,就已经很成功了。因此在降低需求后,你就可以大体没有后顾之忧的去追求成就感。这种个成就感就是选一项事业,不断的去实践,为自己设立关卡,去想办法突破,随之而来的成就感会带你获得更多。

看完这篇文章,觉得自己应该静下心来真正去掌握一门技能,先不想太多,立马动手实践。当天下午,就写了一篇文章,说要在20多天能完成编程100小时的挑战。时间过得很快,浑浑噩噩而的度日和尽力做事都会有这样的感觉,不过后者让你更安心些。11天过去了,里最后的截止日期过了一半,累计的编程时间也达到了50小时。在这里,为自己的前半程做个梳理和总结,希望有助于自己完成挑战,真正提高编程能力,也希望能把好习惯持续下去,做出作品来。

那么50小时内我做了什么呢?

  • 基本上浏览了Flask文档基础使用部分的内容。
  • 过了《Flask Web开发》一书的第一部分。
  • 实现了Flask文档的教学案例。
  • 学习了Flask源码里的minitwit案例。
  • 仿照minitwit制作了miniweibo并在heroku上线。
  • 学习了SQLite的基本用法
  • 学习了flask的三个插件:Bootstrap,WTForms,SQLAlchemy
  • 学习了较大程序的结构,并应用到自己的博客开发上。

现在达到的水平是,了解Web程序的大体创建流程。能完成基本的,程序功能设计,模型、控制器、模板实现,部署上线。在这个过程中,基本都会自己敲代码,把功能跑一下,同时建立了定位问题并尝试解决的习惯。

这11天50小时中,基本安工作日8小时要求自己,期间有4天是周末。大体感觉每天完成8小时纯学习是难度很高的。因为会有很多生活琐事干扰,同时注意力持续时间也是有限的。学习实践中,也有很多问题会拖慢进度,影响情绪,所以从大局上把握自己的进度调整情绪,是很重要的能力。

在学习的过程中实在不能理解的问题就记下来,大概率后面会再次换个角度碰到,极有可能这时你已经掌握足够的知识,可以理解了。很多时候学不下去,或者觉得只会机械的敲示例代码,这时不妨打开一个文档,为学习的内容写一个CheatSheet边学边总结,帮助自己进入学习状态,提高理解的同时也方便后续查阅。

任何技能的习得都是一个积累的过程,不可能一蹴而就。每天花时间去看一些新概念,敲几段范例代码,看看相关文章。短短10天已有体验到积累的妙处,经年累月,能掌握的量级肯定是惊人的。

编程是一种工具,你学会了编程也仅仅是多掌握了一种技能。你终究需要使用这种技能,去解决问题才行。所以首要的是发现问题、提出方案、给出实现的能力。编程只是其中的一个环节,要去观察周边世界,找到待解决的问题,否则编程这项技能没法实践,没法发挥作用,很快就会被你淡忘。

通过10多天的亲自体验,我发现养成一项习惯是容易的,但是能不能坚持,就看这项习惯对你来说多有用。这种有用是对你的生活产生了明显的效果。健身能坚持下去,是你的身材明显变好了、精力更加充沛,让你有自信,自然而然坚持下去。阅读能坚持下去,是它给你灵感的启发,在独自的阅读时体验到和作者沟通的愉悦感,自然而然就读了下来。对于编程,这个挑战才进行到一半,愿自己通过每日计划总结的方式推进自己完成,更重要的是在完成计划后,利用它来解决实际的问题,把编程的习惯保持下去。

时间是不公平的,你的一天多少时间是有目的性的,又有多少时间是随意的任其流逝呢?永远问自己什么事情是重要的,花更多的精力去实践它,不要浪费时间到无用的地方。或者说,要把生命浪费在美好的事情上。这就是100小时编程挑战在到达一半时,我最深刻的体会。

我们总是热血的提出短期计划,但是实现过后呢?我们都习惯在均匀流逝的时间里,庆祝某些节点。但远看,每个日子只要尽力充实它,就都值得庆祝。

愿我们都能,耐心积累。愿我们都能,把生命浪费在美好的事情上。

博客项目设计

博客程序是Web应用中最常见的一种。虽然现在微信公众账号以及其他的一些写作平台非常流行,而且无需搭建方便易用。但是独立的Web博客与之相比,依然有很多优点。

首先,与公开的写作平台不同,独立博客的可定制性强,如果你用微信公众号,你只能发表文章,并使用微信提供的简单接口,可扩展性不强。其次,如果用于些技术文章,微信或者某些平台对于代码等的显示支持性不好。还有很重要的一点,作为Web应用,天然跨平台,你只要使用浏览器就可以访问,而且相对于微信的封闭,独立博客是十分开放的。

博客的本质

博客的主要功能,就是用来发表文章。那为什么写文章呢?首先为了记录自己的行动,再者是表达分享观点和想法。表达自己,让互联网上的陌生人了解你。

可见,博客究其本质,就是互联网上公开发表,个人行动记录和观点的平台。更精简的表达为,博客即公开写作的个人平台。

博客的两种使用视角

博客的使用者主要分两种人。一种是博主自己。另一种是读者。生产者和消费者。生产者注重效率和平台的风格特性,消费者则注重浏览体验和阅读后感想表达渠道。

博主视角功能

博客是一张名片,让人看一眼就知道你在做什么,对什么感兴趣,有那些成就。博客是一本书,包罗了你的个人观点,陈述了你的价值观,让别人知道你是什么样的人。博客是一册传记,记录了你的学习、实践历程,如果读者愿意,甚至可以循着的走过的路径修炼技能。

博客定位,即产生什么内容。技术博客?发表观点?分享好东西?记录行动实践?通向个人其他网络足迹的导航?其实可以包罗万象。

读者视角功能

好的博客必定是能对读者产生触动的博客。这种触动可能是为读者掌握技能提供了参考,也可能是被你的观点说服。通过好内产生的冲动,有可能转化成一种表达或分享的欲望。因此博客需要提供,评论、分享甚至点赞的功能。写作是一种相当有效的社交,通过发表文章,会吸引来与你兴趣类似或者目标相近的人,因此要留下联系方式,方便潜在的朋友联系你。个人介绍必不可少,可以让读者进一步了解你,对你产生兴趣。

总结博客的功能

一个博客最基本的功能就是呈现文章。文章多了以后可能需要文章目录。首页并不能显示下所有文章,因此分页功能是必要的。关于更新博文,其一可以在网站后台直接写作,或者上传本地文件,支持MarkDown是必不可少的。个人简介和联系方式可以让博客显得更正式完整。单篇文章的阅读页需要为读者提供,评论、点赞和分享的功能。有后台就会对应有登录木块。

Flask程序结构

在编写小型Web程序时我们可以使用单一文件,但是当程序较复杂是,这种结构会导致很多问题。当我们在开发Flask Web程序时,可以使用包和模块组织源码。

1. 项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|- myproject
|- app/
|- template/
|- static/
|- main/
|- __init__.py
|- errors.py
|- forms.py
|- views.py
|- __init__.py
|- email.py
|- models.py
|- migrations/
|- tests/
|- __init__.py
|- test*.py
|- venv/
|- requirements.txt
|- config.py
|- manage.py

2. 配置选项

使用配置文件,为开发、测试和生产环境进行不同的设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import os
basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
GRITY_MAIL_SUBJECT_PREFIX = '[Grity]'
GRITY_MAIL_SENDER = 'Grity Admin <grity@example.com>'
GRITY_ADMIN = os.environ.get('GRITY_ADMIN')

@staticmethod
def init_app(app):
pass

class DevelopmentConfig(Config):
DEBUG = True
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
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')

class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlte')

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

'default': DevelopmentConfig
}

3. 程序包

  • 使用程序工厂函数

使用单文件开发的Flask应用,其程序在全局作用域中创建,所以无法动态修改配置文件。运行脚本时,程序实例已经创建了。为了提高测试覆盖度,必须在不同的配置环境中运行程序,所以必须延迟创建程序实例,把程序创建过程移到可显式调用的工厂函数中。使用这种方法还可可以创建多个程序实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/__init__.py
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_sqlalchemy import SQLAlchemy
from config import config

bootstrap = Bootstrap()
db = SQLAlchemy()

def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config.[config_name])
config[config_name].init_app(app)

bootstrap.init_app(app)
db.init_app(app)

# add routes and errorhandlers

return app
  • 在Blueprint中实现程序功能

Blueprint包的构造文件

1
2
3
4
5
6
# app/main/__init__.py
from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors

把Blueprint注册到程序上

1
2
3
4
5
6
7
# app/__init__.py
def create_app(config_name):
# ...
from .mian import main as main_blueprint
app.register_blueprint(main_blueprint)

return app

Blueprint中的错误处理程序

1
2
3
4
5
6
7
8
9
10
11
# app/mian/errors.py
from flask import render_template
from . import main

@main.app_errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404

@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500

Blueprint中的路由定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# app/main/views.py
from flask import render_template, session, redirect, url_for

from . import main
from .forms import NameForm
from .. import db
from ..models import User

@main.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
# ...
return redirect(url_for('.index'))
return render_template('index.html', form=form, name=session.get('name'),
known=session.get('known', False))

4. 启动脚本

顶级文件夹中的manage.py文件用于启动程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Role
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
manager.run()

5. 需求文件

程序中需要包含一个requirements.txt文件,用于记录所有依赖包及其版本号。这样就可以在另外一台电脑上重新生成虚拟环境。

1
(venv) $ pip freeze > requirements.txt

如要创建这个虚拟环境的副本,运行如下命令:

1
(venv) $ pip install -r requirements.txt

6. 单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import unittest
from flask import current_app
from app import create_app, db

class BasicsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()

def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()

def test_app_exists(self):
self.assertFalse(current_app is None)

def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])

在manage.py脚本中添加test自定义命令

1
2
3
4
5
@manager.command
def test():
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)

7. 创建数据库

1
2
3
4
5
6
$ python manage.py db init

$ python manage.py db migrate -m "initial migrate"

$ python manage.py db upgrade
# downgrade

Flask中使用SQLAlchemy

在Web程序中一般采用数据库来保存数据,需要时再向数据库发出查询或者修改。使用的数据库一般为关系型数据库。关系型数据库把数据存储在表中,表模拟程序中不同«»的实体。表的列定义了实体的数据属性,而行则是实体的各条数据项。表和表之间的行可以通过外键进行连接。在Flask中,我们可以才用关系型数据框架SQLAlchemy来作为ORM提升开发效率。

1. 配置数据库

1
2
3
4
5
6
7
8
9
10
from flask_sqlalchemy import SQLAlchemy

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

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True

db = SQLAlchemy(app)

2. 定义模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)

def __repr__(self):
return '<Role %r>' % self.name

class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, index=True)

def __repr__(self):
return '<User %r>' % self.username

3. 关系

1
2
3
4
5
6
7
class Role(db.Model):
# ...
users = db.relationship('User', backref='role')

class User(db.Model):
# ...
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))

4. 数据库操作

  • 创建表
1
2
3
4
$ python hello.py shell
>>> from hello import db
>>> db.drop_all()
>>> db.create_all()
  • 插入行
1
2
3
4
5
6
7
8
9
10
>>> from hello import Role, User
>>> admin_role = Role(name='Admin')
>>> mod_role = Role(name='Moderator')
>>> user_role = Role(name='User')
>>> user_john = User(username='john', role=admin_role)

>>> db.session.add_all([admin_role, mod_role, user_role, user_john])
>>> db.session.commit()

>>> db.session.rollback()
  • 修改行
1
2
3
>>> admin_role = 'Administrator'
>>> db.session.add(admin_role)
>>> db.session.commit()
  • 删除行
1
2
>>> db.session.delete(mod_role)
>>> db.session.commit()
  • 查询
1
2
3
4
5
6
7
8
9
10
11
>>> Role.query.all()
>>> User.query.all()

>>> User.query.filter_by(role=user_role).all()
>>> str(User.query.filter_by(role=user_role))

>>> user_role = Role.query.filter_by(name='User').first()

# lazy="dynamic"
>>> user_role.users.order_by(User.name).all()
>>> user_role.users.count()

5. 集成Python shell

1
$ pip install flask-script
1
2
3
4
5
6
7
from flask.ext.script import Shell, Manager

def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)

manager = Manager(app)
manager.add_command("shell", Shell(make_context=make_shell_context))
1
2
3
$ python hello.py shell
>>> app
>>> db

6. 使用Flask-Migrate实现数据库迁移

1
$ pip install Flask-Migrate
  • 创建迁移仓库
1
2
3
4
from flask.ext.migrate import Migrate, MigrateCommand
# ...
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
1
$ python hello.py db init
  • 创建迁移脚本
1
$ python hello.py db migrate -m "initial migrate"
  • 更新数据库
1
2
$ python hello.py db upgrade
# downgrade

Flask中使用Web表单

在Flask中通过request.form以获取Web表单数据。但是生成表单的HTML代码和验证提交表单的数据,麻烦而且要重复操作。Flask-WTF扩展对WTForms包进行了包装,可以简化Flask中表单的处理。

1. 跨站请求伪造保护

WTF扩展可以使用配置中的通用密钥,生成加密令牌,再用令牌验证求情中表单数据的真伪。通常为了增加安全性,密钥不应直接写入代码,而要保存在环境变量中。

1
2
3
4
5
6
app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

# get SECRET_KEY from environment
import os
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'

2. 表单类

在使用Flask-WTF时,每个Web表单都由一个继承自Form的类表示。这个类定义了表单的字段,和本字段的验证方法,验证方法用来验证用户提交的输入值是否符合要求。

1
2
3
4
5
6
7
from flask_wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required

class NameForm(Form):
name = StringField('What is your name?', validators=[Required()])
submit = SubmitField('Submit')

3. 把表单渲染成HTML

1
2
3
4
5
<form method="POST">
{{ form.hidden_tag() }}
{{ form.name.label }} {{ form.name(id='my-text-field') }}
{{ form.submit() }}
</form>

这样渲染表单依旧很麻烦,而在Flask-Bootstrap中有预先定义好的样式,可以直接渲染整个Flask-WTF表单。

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

4. 在视图函数中处理表单

视图函数中需要渲染表单,接收处理表单数据。

1
2
3
4
5
6
7
8
@app.route('/', methods=['GET', 'POST'])
def index():
name = None
form = NameForm()
if form.validate_on_submit():
name = form.name.data
form.name.data = ''
return render_template('index.html', form=form, name=name)

5. 重定向和用户会话

以上方法构建的页面有一个问题。在刷新提交过表单的页面后,会有一个再次调教表单的确认信息。这是因为,刷新后浏览器会自动提交最后一次请求,即之前的POST请求。它影响了使用体验,同时也让不了解的用户产生疑惑。一个解决办法就是在完成POST请求后重定向,再发送一个GET请求。

但是在之后的GET请求中我们就无法获取POST的表单数据。此时可以利用用户会话Session,将数据加密存储于客户端的cookie中,方便程序的请求间「记住」数据。

1
2
3
4
5
6
7
8
9
10
from flask import render_template, redirect, session, url_for

@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm
if form.validate_on_submit():
session['name'] = form.name.data
return redirect(url_for('index'))
return render_template('index.html', form=form,
name=session.get('name'))

利用Flask创建一个mini微博(4)

利用Flask创建一个mini微博(1)
利用Flask创建一个mini微博(2)
利用Flask创建一个mini微博(3)

Step 5:测试程序

参考:
Flask/Docs/Testing Flask Application
minitwit/test

没有经过测试的程序,在后续的修改和添加功能时很难处理。如果一个程序有自动测试,你就可以放心的秀敢程序,然后通过运行测试就知道新的修改对原有功能有没有影响。

Flask提供了一个测试客户端来处理应用上下文的问题。有了它你就可以使用你自己的测试方案了。这里我们将使用Python标准库unittest模块进行测试。

测试框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# miniweibo_tests.py
import os
import miniweibo
import unittest
import tempfile

class FlaskTestCase(unittest.TestCase):

def setUp(self):
self.db_fd, miniweibo.app.config['DATABASE'] = tempfile.mkstemp()
miniweibo.app.config['TESTING'] = True
self.app = miniweibo.app.test_client()
with miniweibo.app.app_context():
miniweibo.init_db()

def tearDown(self):
os.close(self.db_fd)
os.unlink(miniweibo.app.config['DATABASE'])

if __name__ == '__main__':
unittest.main()

tempfile.makstemp()随机生成一个文件,返回文件操作实例和文件名。设置TESTING变量后,程序就不会捕捉请求时发生的错误,以便最后一起输出测试结果。在测试开始初始化数据库。测试完后关闭数据库并删除临时文件。

为了测试时方便调用,在测试实例前编写一些辅助函数。比如我们要测试注册的功能,首先编写注册的辅助函数,然后在测试注册时出现的各种可能性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def register(self, username, password, password2=None, email=None):
"""Helper function to register a user"""
if password2 is None:
password2 = password
if email is None:
email = username + '@example.com'
return self.app.post('/register', data={
'username': username,
'password': password,
'password2': password2,
'email': email
}, follow_redirects=True)

def test_register(self):
"""Make sure registering works"""
rv = self.register('user1', 'default')
assert 'You were successfully registered and can login now' in rv.data
rv = self.register('user1', 'default')
assert 'The username is already taken' in rv.data
rv = self.register('', 'default')
assert 'You have to enter a username' in rv.data
rv = self.register('meh', '')
assert 'You have to enter a password' in rv.data
rv = self.register('meh', 'x', 'y')
assert 'The two password do not match' in rv.data
rv = self.register('meh', 'foo', email='broken')
assert 'You have to enter a valid email address' in rv.data

接下来测试登录、登出功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def login(self, username, password):
"""Helper function to sign in"""
return self.app.post('/login', data={
'username': username,
'password': password,
}, follow_redirects=True)

def register_and_login(self, username, password):
"""Register an account then login"""
self.register(username, password)
return self.ogin(username, password)

def logout(self):
"""Helper function to logout"""
return self.app.get('/logout', follow_redirects=True)

def test_login_logout(self):
"""Make sure logging in and logging out works"""
rv = self.register_and_login('user1', 'default')
assert 'You were logged in' in rv.data
rv = self.logout()
assert 'You were logged out' in rv.data
rv = self.login('user1', 'wrongpassword')
assert 'Invalid password' in rv.data
rv = self.login('user2', 'default')
assert 'Invalid username' in rv.data

测试发表微博功能:

1
2
3
4
5
6
7
8
9
10
11
def add_message(self, text):
"""Helper function to add a message"""
rv = self.app.post('/add_message', data={'text': text},
follow_redirects=True)
return rv

def test_add_message(self):
"""Check if adding messages works"""
self.register_and_login('foo', 'default')
rv = self.add_message('test message 1')
assert 'test message 1' in rv.data

测试时间线和关注功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def test_timelines(self):
"""Make sure that timelines work"""
self.register_and_login('foo', 'default')
self.add_message('the message by foo')
self.logout()
self.register_and_login('bar', 'default')
self.add_message('the message by bar')
rv = self.app.get('/public')
assert 'the message by foo' in rv.data
assert 'the message by bar' in rv.data

# test bar's timeline
rv = self.app.get('/')
assert 'the message by foo' not in rv.data
assert 'the message by bar' in rv.data

# test follow function
rv = self.app.get('/foo/follow', follow_redirects=True)
assert 'You are now following &#34;foo&#34;' in rv.data

rv = self.app.get('/')
assert 'the message by foo' in rv.data
assert 'the message by bar' in rv.data

# test the user timeline
rv = self.app.get('/bar')
assert 'the message by foo' not in rv.data
assert 'the message by bar' in rv.data
rv = self.app.get('/foo')
assert 'the message by foo' in rv.data
assert 'the message by bar' not in rv.data

# test unfollow function
rv = self.app.get('/foo/unfollow', follow_redirects=True)
assert 'You are no longer following &#34;foo&#34;' in rv.data
rv = self.app.get('/')
assert 'the message by foo' not in rv.data
assert 'the message by bar' in rv.data

Step 6: 部署程序

Web app在经过测试后,就可以部署上线了,以便用户通过互联网进行访问。下面我们将使用Heroku来部署我们的MiniWeibo。

  1. 安装、注册Heroku

首先下载安装Heroku Toolbelt。然后注册一个免费的账号。在终端中登录:

1
$ heroku login

  1. 配置部署文件

尽管Flask为我们提供了内置的wsgi服务器,但是在实际生产环境中,我们需要更换一个生产环境可用的服务器。我们选择gunicorn。

1
$ pip install gunicorn

接下来我们设置部署需要的Procfile文件

1
web gunicorn miniweibo:app

除了Procfile文件,还要自动创建一个Python依赖包文件。

1
pip freeze > requirements.text

  1. 利用git提交所有修改
1
2
$ git add -A
$ git commit -m "Add files for deploying at Heroku"
  1. 创建Heroku App并提交部署
1
2
$ heroku create miniweibo
$ git push heroku master

在完成部署后,Heroku会给出已部署的web应用的地址。
接下来我们在浏览器中输入mini-weibo.herokuapp.com就可以访问我们的MiniWeibo啦!

AJAX with jQuery in Flask

  1. Loading jQuery
1
2
<script type=text/javascript src="{{
url_for('static', filename='jquery.js') }}">
</script>

or

1
2
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{{url_for('static', filename='jquery.js') }}">\x3C/script>')</script>

  1. Get URL Root
1
2
3
<script type=text/javascript>
$SCRIPT_ROOT = {{ request.script_root|tojson|safe }};
</script>

  1. JSON View Functions
1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, jsonify, render_template, request
app = Flask(__name__)

@app.route('/_add_numbers')
def add_numbers():
a = request.args.get('a', 0, type=int)
b = request.args.get('b', 0, type=int)
return jsonify(result=a + b)

@app.route('/')
def index():
return render_template('index.html')
  1. The HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script type=text/javascript>
$(function() {
$('a#calculate').bind('click', function() {
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
a: $('input[name="a"]').val(),
b: $('input[name="b"]').val()
}, function(data) {
$("#result").text(data.result);
});
return false;
});
});
</script>

<h1>jQuery Example</h1>
<p><input type=text size=5 name=a> +
<input type=text size=5 name=b> =
<span id=result>?</span>
<p><a href=# id=calculate>calculate server side</a>

Flask/Docs/AJAX with jQuery