分类目录归档:计算机与编程

使用Flask-Avatars在Flask项目中设置头像

Flask-Avatars

大多数Web程序中都会涉及到头像的实现。不同类型的项目,对于头像的需求不同,有些项目可以直接使用Gravatar提供的头像服务,而有的项目则需要提供自定义头像设置。扩展Flask-Avatars几乎满足了所有常见的头像需求:

  • 默认头像
  • Gravatar头像
  • Robohash头像
  • 社交网站头像
  • 生成随机图案头像Identicon
  • 图片裁剪头像

Flask-Avatars

GitHub主页:https://github.com/greyli/flask-avatars

安装与初始化

使用pip安装:

$ pip install flask-avatars

像其他扩展类似,你需要实例化从flask_avatars导入的Avatars类,并传入app实例:

from flask_avatars import Avatars

app = Flask(__name__)
avatars = Avatars(app)

如果你使用工厂函数创建程序实例,那么这里也可以不传入程序实例app,在工厂函数中对这个avatars对象调用init_app()方法时再传入app实例。

默认头像

Flask-Avatars内置了几个默认头像,可以用来作为用户注册后的初始头像,或是作为博客评论者的头像。在模板中调用avatars.default()即可获取URL:

<img src="{{ avatars.default() }}">

你可以通过size参数来设置尺寸,默认为m,其他可选值有l和s。实际的调用示例如下所示:default avatar

 

Gravatar

在模板中调用avatars.gravatar()方法并传入Email散列值即可获取Gravatar(gravatar.com)的头像URL:

<img src="{{ avatars.gravatar(email_hash) }}">

Email散列值可以通过下面的方式获取:

import hashlib

avatar_hash = hashlib.md5(my_email.lower().encode('utf-8')).hexdigest()

实际的调用示例如下所示:gravatar

Robohash

Robohash(robohash.org)是一个生成随机机器人头像的服务(目前Gravatar的默认头像中已经支持这一类型,可以通过将default参数设为robohash获取)。在模板中调用avatars.robohash()并传入随机文本作为参数即可获取Robohash的头像URL:

<img src="{{ avatars.robohash(some_text) }}">

实际的调用示例如下所示:

robohash

社交网站头像

Flask-Avatars通过Avatars.io提供了社交头像获取服务,目前支持Facebook、Twitter、Instagram和Gravatar。通过在模板中调用avatars.social_media()方法并传入用户名(username)即可获取对应的头像URL,通过size参数可以设置尺寸,可选值为small、medium和large:

<img src="{{ avatars.social_media(username) }}">

通过platform参数可以设置平台,默认为twitter,可选值为twitter、facebook、instagram和gravatar:

<img src="{{ avatars.social_media(username, platform='facebook') }}">

实际的调用示例如下所示:

avatars.io

生成随机图案头像Identicon

Gravatar服务可能会有不稳定的情况,这种情况下,你可以在本地为用户生成头像(identicon),下面是一个简单的示例:

app.config['AVATARS_SAVE_PATH '] = 'the/path/to/save/avatar'

avatar = Identicon()
filenames = avatar.generate(text=‘一串唯一字符’)

avatar.generate()会根据传入的随机字符创建三个尺寸(可以通过配置变量AVATARS_SIZE_TUPLE自定义)的头像,保存到指定的位置,并返回三个头像文件名。你可以将这个文件名保存到数据库中,并创建一个视图函数来提供头像文件:

from flask import send_form_directory, current_app

@app.route('/avatars/<path:filename>')
def get_avatar(filename):
    return send_from_directory(current_app.config['AVATARS_SAVE_PATH'], filename)

实际生成的头像示例如下所示:identicon

裁剪头像

Flask-Avatars基于Jcrop提供了头像裁剪功能,具体步骤可以参考文档中的相关部分。下面是示例程序中的裁剪页面:裁剪

 

裁剪后的结果:裁剪完成

配置

Flask-Avatars支持的配置列表如下所示:

配置 默认值 说明
AVATARS_GRAVATAR_DEFAULT identicon Gravatar默认头像类型
AVATARS_SAVE_PATH None 头像保存路径
AVATARS_SIZE_TUPLE (30, 60, 150) 头像尺寸三元素元组,格式为 (small, medium, large),用于生成identicon头像和裁剪头像
AVATARS_IDENTICON_COLS 7 Identicon头像一行的色块数量
AVATARS_IDENTICON_ROWS 7 Identicon头像一列的色块数量
AVATARS_IDENTICON_BG None Identicaon头像的背景颜色,传入RGB元组,比如(125, 125, 125)。默认使用随机颜色
AVATARS_CROP_BASE_WIDTH 500 裁剪图片的显示宽度
AVATARS_CROP_INIT_POS (0, 0) 裁剪框起始位置,两元素元组(x, y),默认为左上角
AVATARS_CROP_INIT_SIZE None  裁剪框的尺寸,默认为尺寸元组中设置的l尺寸大小,即AVATARS_SIZE_TUPLE[0]
AVATARS_CROP_MIN_SIZE None 裁剪框的限制最小尺寸,默认无限制
AVATARS_CROP_PREVIEW_SIZE None 预览窗口的尺寸,默认为尺寸元组中设置的m尺寸大小,即AVATARS_SIZE_TUPLE[1]
AVATARS_SERVE_LOCAL False 是否从本地加载Jcrop资源,默认从CDN加载

示例程序

Flask-Avatars的Git仓库中包含三个实例程序,也就是文中的截图对应的程序:

  • examples/basic —— 基本示例
  • examples/identicon —— Identicon示例
  • examples/crop —— 裁剪示例

你可以通过下面的方式来运行实例程序:

$ git clone https://github.com/greyli/flask-avatars.git
$ cd flask-avatars/examples
$ pip install flask flask-avatars
$ cd basic  # 切换到identicon和crop目录可以运行对应的示例程序
$ flask run

这篇文章属于“Flask常用扩展介绍系列”,这个系列的文章目录索引可以在《Flask常用扩展介绍系列文章索引》看到。

相关链接

使用Flask-SQLAlchemy调用create_all()创建数据库表前是否需要导入模型类?

当继承db.Model基类的子类被声明创建时,根据db.Model基类继承的元类中设置的行为,类声明后会将表信息注册到db.Model.metadata.tables属性中。

create_all()方法被调用时正是通过这个属性来获取表信息。因此,当我们调用create_all()前,需要确保模型类被声明创建。如果模型类存储在单独的模块中,不导入该模块就不会执行其中的代码,模型类便不会被创建,进而便无法注册表信息到db.Model.metadata.tables中,所以这时需要导入相应的模块。

因为我们的目的是让模型类被创建,所以不论是导入整个模块还是导入其中某个模型类都可以,并不需要导入全部模型类。

一般情况下,我们不用关心这个问题。在单脚本的Flask程序中自不必说,在使用包组织的Flask程序中,创建程序实例时必然需要导入视图函数所在的模块,或是蓝本所在的模块,而这些模块会导入模型类。

下面我们会通过解析源码简单了解模型类对应的表信息是如何注册到db.Model.metadata.tables中的。

不论是单独使用SQLAlchemy时创建的Base基类,还是使用Flask-SQLAlchemy时创建db对象后的db.Model基类,都是通过declarative_base()函数创建,具体源码如下:

def declarative_base(bind=None, metadata=None, mapper=None, cls=object,
                     name='Base', constructor=_declarative_constructor,
                     class_registry=None,
                     metaclass=DeclarativeMeta):

    lcl_metadata = metadata or MetaData()
    if bind:
        lcl_metadata.bind = bind

    if class_registry is None:
        class_registry = weakref.WeakValueDictionary()

    bases = not isinstance(cls, tuple) and (cls,) or cls
    class_dict = dict(_decl_class_registry=class_registry,
                      metadata=lcl_metadata)

    if isinstance(cls, type):
        class_dict['__doc__'] = cls.__doc__

    if constructor:
        class_dict['__init__'] = constructor
    if mapper:
        class_dict['__mapper_cls__'] = mapper

    return metaclass(name, bases, class_dict)

源码位置:github.com/zzzeek/sqlal

这个函数返回一个元类实例,对应的元类为DeclarativeMeta,这个元类定义了一些特殊行为:

class DeclarativeMeta(type):
    def __init__(cls, classname, bases, dict_):
        if '_decl_class_registry' not in cls.__dict__:
            _as_declarative(cls, classname, cls.__dict__)
        type.__init__(cls, classname, bases, dict_)

    def __setattr__(cls, key, value):
        _add_attribute(cls, key, value)

    def __delattr__(cls, key):
        _del_attribute(cls, key)

源码位置:github.com/zzzeek/sqlal

_as_declarative()函数以及附加的其他多层调用为这个类进行了更多设置,比如添加__table__属性为存储表信息的Table对象,设置metadata属性等等。

其中魔法方法__setattr__()中调用了_add_attribute()函数,这个函数执行了一系列模型类属性的注册操作,其中的一个操作便是向基类 __table__属性指向的Table对象调用append_column()方法添加表字段信息:

def _add_attribute(cls, key, value):
    if '__mapper__' in cls.__dict__:
        if isinstance(value, Column):
            _undefer_column_name(key, value)
            cls.__table__.append_column(value)
            cls.__mapper__.add_property(key, value)
        ...

源码位置:github.com/zzzeek/sqlal

经过这一系列注册操作,表信息就被添加到db.Model.metadata.tables属性中,这个属性返回包含所有表信息的字典(表名称与Table实例的映射),下面是一个示例:

immutabledict({'comment': Table('comment', MetaData(bind=None), Column('id'
, Integer(), table=<comment>, primary_key=True, nullable=False), Column('bo
dy', Text(), table=<comment>), Column('timestamp', DateTime(), table=<comme
nt>, default=ColumnDefault(<function utcnow at 0x03D2E3F0>)), Column('flag'
, Integer(), table=<comment>, default=ColumnDefault(0)), Column('author_id
', Integer(), ForeignKey('user.id'), table=<comment>), Column('photo_id', I
nteger(), ForeignKey('photo.id'), table=<comment>), schema=None)})             

db.Model.metadata存储MetaData类实例,MetaData类实例存储Table类实例的集合,而Table类实例存储模型类对应的数据库表字段信息,可以通过模型类的__table__属性获取。create_all()方法实际调用的是db.Model.metadata.create_all 方法,这个方法会将Table类实例存储的信息转换为数据库模式(通过sqlclchemy.sql.ddl.SchemaGenerator)。

顺便说一句,因为表信息存储在特定的基类中,所以为了正确创建数据库表,你需要对模型类继承的基类调用create_all()方法,即db.create_all(),或是Base.metadata.create_all(engine)。如果你在测试时新创建一个db对象或是Base基类,那么它是不会包含表信息的。

这篇文章来自我在知乎写的这个回答,作为备份。

使用Bootstrap-Flask在Flask项目中集成Bootstrap

Bootstrap-Flask是一个简化在Flask项目中集成前端开源框架Bootstrap过程的Flask扩展。使用Bootstrap可以快速的创建简洁、美观又功能全面的页面,而Bootstrap-Flask让这一过程更加简单和高效。尤其重要的是,Bootstrap-Flask支持最新版本的Bootstrap 4版本。

Bootstrap-Flask logo

Bootstrap-Flask logo

GitHub项目主页:https://github.com/greyli/bootstrap-flask

和Flask-Bootstrap的区别

简单来说,Bootstrap-Flask的出现是为了替代不够灵活且缺乏维护的Flask-Bootstrap。它的主要设计参考了Flask-Bootstrap,其中渲染表单和分页部件的宏基于Flask-Bootstrap中的相关代码修改实现。和Flask-Bootstrap相比,前者有下面这些优点:

  • 去掉了内置的基模板,换来更大的灵活性,提供了资源引用代码生成函数
  • 支持Bootstrap 4
  • 标准化的Jinja2语法
  • 提供了更多方便的宏,比如简单的分页导航部件、导航链接等
  • 宏的功能更加丰富,比如分页导航支持传入URL片段
  • 统一的宏命名,即“render_*”,更符合直觉

安装与初始化

你如果使用过Flask-Bootstrap,那么除了安装时的名称外,这个过程基本没有不同。

安装:

$ pip install bootstrap-flask

初始化:

from flask_bootstrap import Bootstrap
from flask import Flask

app = Flask(__name__)

bootstrap = Bootstrap(app)

如果你使用工厂函数创建程序实例,那么可以使用下面的方式初始化扩展:

from flask_bootstrap import Bootstrap
from flask import Flask

bootstrap = Bootstrap()

def create_app():
    app = Flask(__name__)
    bootstrap.init_app(app)
    
    return app

Bootstrap-Flask提供了哪些功能

2个资源加载函数

在简单的示例程序中,或是在开发时,你可以使用它提供的两个快捷方法来生成Bootstrap资源引用代码,如下所示:

<head>
{{ bootstrap.load_css() }}
</head>
<body>
...
{{ bootstrap.load_js() }}
</body>

7个快捷渲染宏

目前,Bootstrap-Flask一共提供了7个宏,分别用来快捷渲染各类Bootstrap页面组件,并提供了对扩展Flask-WTF、Flask-SQLAlchemy的支持。

模板路径 说明
render_field() bootstrap/form.html 渲染一个WTForms表单字段
render_form() bootstrap/form.html 渲染一个WTForms表单类
render_pager() bootstrap/pagination.html 渲染一个简单分页导航,包含上一页和下一页按钮
render_pagination() bootstrap/pagination.html 渲染一个标准分页导航部件
render_nav_item() bootstrap/nav.html 渲染一个导航条目
render_breadcrumb_item() bootstrap/nav.html 渲染一个面包屑条目
render_static() bootstrap/utils.html 渲染一个资源引用语句,即 <link><script>标签语句

使用方法相当简单,以渲染Flask-WTF(WTForms)的表单类的render_form()宏为例,你只需要从对应的模板路径导入宏,然后调用即可并传入必要的参数:

{% from 'bootstrap/form.html' import render_form %}

{{ render_form(form) }}

你可以在项目仓库的examples目录下找到一个完整的示例程序,示例程序的运行方式如下:

$ git clone https://github.com/greyli/bootstrap-flask.git
$ pip install flask flask-wtf flask-sqlalchemy bootstrap-flask
$ cd bootstrap-flask/examples
$ flask run

现在访问http://localhost:5000即可看到示例程序主页。示例程序包含所有宏的实际调用示例,其中分页宏示例页面如下图所示:

分页宏示例

分页宏示例

欢迎贡献代码

这个项目还刚刚起步,各方面都有需要完善的地方,近期我会为它编写一份完善的文档,欢迎有兴趣的朋友贡献代码

WTForms自定义验证方法(行内验证器)是如何被调用的?

这篇文章基于我在知乎上的这个回答,进行了相应的简化处理,放到这里做个备份。

万能的回答

答案在源码里。

简单的回答

WTForms会在你对表单实例调用Form.validate()方法时收集所有的行内验证方法,然后在对每个字段调用Field.validate()方法验证时将这些自定义行内验证方法一并和通过validators参数传入的验证器列表一起调用,进行验证。因为WTForms在调用时把对应的field作为参数传入了行内验证方法,所以你可以在自定义验证方法中通过field.data获取对应字段的数据。

深入的回答

WTForms会在你对表单实例调用Form.validate()方法时收集所有的行内验证方法。在Form类中的validate()方法定义中,你可以看到WTForms是如何收集这些行内验证方法的:

class Form(with_metaclass(FormMeta, BaseForm)): 
   ....
   def validate(self):
        extra = {}
        for name in self._fields:
            inline = getattr(self.__class__, 'validate_%s' % name, None)
            if inline is not None:
                extra[name] = [inline]

        return super(Form, self).validate(extra)

源码位置:github.com/wtforms/wtfo

这里迭代所有的字段属性,然后表单类中是否包含validate_字段名形式的方法。如果有,那么就添加到extra字段里,这个字段被传递到BaseForm类的validate()方法中。在BaseForm类的validate()方法中,WTForms迭代所有字段,并对每个字段调用Field.validate()方法验证字段,继续传入自定义的行内验证方法:

class BaseForm(object):
    ...
    def validate(self, extra_validators=None):
        self._errors = None
        success = True
        for name, field in iteritems(self._fields):  # 迭代字段名和字段对象
            if extra_validators is not None and name in extra_validators:  # 判断当前迭代字段是否存在自定义验证器
                extra = extra_validators[name]
            else:
                extra = tuple()
            if not field.validate(self, extra):  # 调用字段类的验证方法进行验证,传入自定义验证器
                success = False
        return success

源码位置:github.com/wtforms/wtfo

而在字段基类Field的validate()方法中,WTForms使用itertool模块提供的chain()函数把你实例化字段类时传入的验证器(self.validators)和自定义行内验证器(extra_validators)连接到一起:

class Field(object):    
    ...
    def validate(self, form, extra_validators=tuple()):
        ...
        # Run validators
        if not stop_validation:
            chain = itertools.chain(self.validators, extra_validators)  # 合并两类验证器
            stop_validation = self._run_validation_chain(form, chain)  # 运行验证器
        ...

源码位置:github.com/wtforms/wtfo

连接起来的所有验证器赋值为chain变量,并传入到self._run_validation_chain(form, chain)进行进一步调用:

class Field(object):    
    ...
    def _run_validation_chain(self, form, validators):
        for validator in validators:
            try:
                validator(form, self)  # 传入字段类本身作为第二个参数

源码位置:github.com/wtforms/wtfo

这个方法迭代所有的验证器对字段数据进行验证。关键在于validator(form, self),可以看到这里传入了第二个参数self,即Field类本身,这也是为什么你可以在自定义验证方法中通过field.data获取当前字段数据。

Python中的下划线有多少种用法和含义?

这篇文章来自我在知乎上的这个回答,做个备份。

大概有10种。

如下:

# No.1
# 在交互式解释器中获取上一个语句执行的结果
# 比如:
# >>> 1+1
# 2
# >>> _
# 2
# >>> _ * 5
# 10
_

# No.2
# 用来在函数、模块、包、变量名中分隔单词,增加可读性
var_foo_bar

# No.3
# 内部使用的变量、属性、方法、函数、类或模块(约定)
# from foo import * 不会导入以下划线开头的对象
_var

# No.4
# 避免和保留的关键字冲突(约定)
# 比如:class_、type_
var_

# No.5
# 在类内的私有变量(private)
# 类外部无法直接使用原名称访问
# 需要通过instance._ClassName__var的形式访问(name mangling)
__var

# No.6(这一条存疑)
# 在类内的保护变量
_var_

# No.7
# Python内置的“魔法”方法或属性
# 你也可以自己定义,但一般不推荐
# 比如:__init__, __file__, __main__
__var__

# No.8
# 作为内部使用的一次性变量
# 通常在循环里使用
# 比如:[_ for _ in range(10)]
# 或是用作占位,不实际使用的变量
# 比如:for _, a in [(1,2),(3,4)]: print a
_

# No.9
# i18n里作为gettext()的缩写
_()

# No.10
# 用来分隔数值以增加可读性(Python 3.6新增)
# 比如
# >>> num = 1_000_000 
# >>> num
# 1000000
1_000_000

参考链接:

《Flask Web开发实战》中的实战项目

很多朋友对《Flask Web开发实战》中的项目实例很感兴趣,这篇文章就来简单的对这些项目进行介绍,并给出一些截图。这几个项目的源码和在线Demo链接均可以在helloflask.com看到。

第1~6章及13章:helloflask

Hello, Flask!

第1~6章以及第13章的示例程序统一包含在helloflask仓库中的demos目录下。另外,这个仓库也作为《Flask Web开发实战》的仓库,书的勘误文件等内容也会一并在这里更新。

Flask Web开发实战第6章电子邮件示例程序

Flask Web开发实战第6章电子邮件示例程序

Flask Web开发实战第5章数据库示例程序

Flask Web开发实战第5章数据库示例程序


第7章:留言板 – SayHello

Say hello to the world.

这个项目比较简单,主要用来介绍项目组织和Web程序开发流程,没有复杂功能,介绍了虚拟数据的生成和时间日期的本地化。

SayHello绝对时间弹窗

SayHello绝对时间弹窗

SayHello主页

SayHello主页

第8章:个人博客 – Bluelog

A blue blog.

一个基础的博客程序,使用工厂函数和蓝本组织程序,主要包含下面这些功能点:

  • 使用工厂函数创建程序实例
  • 使用蓝本模块化程序
  • 使用富文本编辑器
  • 创建文章/分类/评论
  • 编辑文章/分类
  • 删除文章/分类/评论
  • 回复评论
  • 管理后台
  • 文章分类
  • 文章分页
  • 博客设置
  • 用户认证
  • 网站主题切换

博客主页

博客主页

更换了主题的博客主页

更换了主题的博客主页

博客后台的文章管理页面

博客后台的文章管理页面

第9章:图片社交网站 – Albumy

Capture and share every wonderful moment.

一个进阶的程序实例,主要包含下面的功能点:

  • 大型项目组织形式
  • 用户注册流程
  • 用户角色和权限管理
  • 图片上传
  • 图片处理
  • 删除确认模态框
  • 图片分类
  • 图片标签
  • 用户资料弹窗
  • 图片收藏
  • 用户关注
  • 在资料弹窗中执行关注操作
  • 消息提醒
  • 消息提醒的实时更新
  • 生成随机头像
  • 用户自定义头像
  • 更改密码
  • 提醒消息开关
  • 收藏可见开关
  • 注销账户
  • 全文搜索
用户个人主页

用户个人主页

网站动态页面

网站动态页面

评论中的用户资料弹窗

评论中的用户资料弹窗

头像裁剪

头像裁剪


第10章:待办事项程序 – Todoism

We are todoist, we use todoism.

一个简单的待办事项程序,使用jQuery实现简单的单页效果,主要包含下面的功能点:

  • 单页程序
  • 国际化和本地化支持
  • 实现Web API
程序主页

程序主页

切换语言后的程序主页

切换语言后的程序主页


第11章:在线聊天室 – CatChat

Chatroom for coders, not cats.

一个使用Flask-SocketIO实现的聊天室,主要包含下面这些功能点:

  • Gravatar头像
  • 实时双向通讯
  • 第三方登录
  • 无限滚动加载历史消息
  • Markdown支持
  • 代码语法高亮
  • 标签页消息提醒
  • 浏览器桌面通知
聊天页面

聊天页面

登录页面

登录页面

代码语法高亮

代码语法高亮


第15章:Flask扩展 – Flask-Share

Create social share component in Jinja2 template based on share.js.

Flask-Share是一个基于share.js实现,可以在模板中方便的创建社交分享组件的扩展。

创建社交分享组件

创建社交分享组件

使用GitHub-Flask实现GitHub第三方登录

这篇文章属于“Flask常用扩展介绍系列”,这个系列的文章目录索引可以在《Flask常用扩展介绍系列文章索引》看到。

前言

这篇文章大部分内容为《Flask Web开发实战》第10章的删减章节,另外摘取了部分书中现有的内容。我为这篇文章单独编写了示例程序,GitHub地址为https://github.com/helloflask/github-login。运行示例程序的步骤如下:

$ git clone https://github.com/helloflask/github-flask.git
$ cd github-login
$ pipenv install --skip-lock  # 如果没有安装pipenv,那么执行pip install pipenv
$ flask run  # 在此之前需要在GitHub注册OAuth程序并将客户端ID与密钥写入程序,具体见下文
如果你想直接体验程序,可以访问部署在PythonAnywhere(“到处都是蛇”)的在线实例
 

附注 第三方登录的原理是与第三方服务进行OAuth认证交互的,这里不会详细介绍OAuth,具体可以阅读OAuth官网列出的资源,另外即将上市的Flask新书里也提供了相关内容。

什么是第三方登录

简单来说,为一个网站添加第三方登录指的是提供通过其他第三方平台账号登入当前网站的功能。比如,使用QQ、微信、新浪微博账号登录。对于某些网站,甚至可以仅提供社交账号登录的选项,这样网站本身就不需要管理用户账户等相关信息。对用户来说,使用第三方登录可以省去注册的步骤,更加方便和快捷。

如果项目和GitHub、开源项目、编程语言等方面相关,或是面向的主要用户群是程序员时,可以仅支持GitHub的第三方登录,比如Gitter、GitBook、Coveralls和Travis CI等。在Flask程序中,除了手动实现,我们可以借助其他扩展或库,我们在这篇文章里要使用的GitHub-Flask扩展专门用于实现GitHub第三方登录,以及与GitHub进行Web API资源交互。

第三方登录授权流程

起这个标题是为了更好理解,具体来说,整个流程实际上是指OAuth2中Authorization Code模式的授权流程。为了便于理解,这里按照实际操作顺序列出了整个授权流程的实现步骤:

  1. 在GitHub为我们的程序注册OAuth程序,获得Client ID(客户端ID)和Client Secret(客户端密钥)。
  2. 我们在登录页面添加“使用GitHub登录”按钮,按钮的URL指向GitHub提供的授权URL,即https://github.com/login/oauth/authorize
  3. 用户点击登录按钮,程序访问GitHub的授权URL,我们在授权URL后附加查询参数Client ID以及可选的Scope等。GitHub会根据授权URL中的Client ID识别出我们的程序信息,根据scope获取请求的权限范围,最后把这些信息显示在授权页面上。
  4. 用户输入GitHub的账户及密码,同意授权
  5. 用户同意授权后GitHub会将用户重定向到我们注册OAuth程序时提供的回调URL。如果用户同意授权,回调URL中会附加一个code(即Authorization Code,通常称为授权码),用来交换access令牌(即访问令牌,也被称为登录令牌、存取令牌等)。
  6. 我们在程序中接受到这个回调请求,获取code,发送一个POST请求到用于获取access令牌的URL,并附加Client ID、Client Secret和code值以及其他可选的值。
  7. GitHub接收到请求后,验证code值,成功后会再次向回调URL发起请求,同时在URL的查询字符串中或请求主体中加入access令牌的值、过期时间、token类型等信息。
  8. 我们的程序获取access令牌,可以用于后续发起API资源调用,或保存到数据库备用
  9. 如果用户是第一次登入,就创建用户对象并保存到数据库,最后登入用户
  10. 这里可选的步骤是让用户设置密码或资料

在GitHub注册OAuth程序

和其他主流第三方服务相同,GitHub使用OAuth2中的Authorization Code模式认证。因为认证后,根据授权的权限,客户端可以获取到用户的资源,为了便于对客户端进行识别和限制,我们需要在GitHub上进行注册,获取到客户端ID和密钥才能进行OAuth授权。

在服务提供方的网站上进行OAuth程序注册时,通常需要提供程序的基本信息,比如程序的名称、描述、主页等,这些信息会显示在要求用户授权的页面上,供用户识别。在GitHub中进行OAuth程序注册非常简单,访问https://github.com/settings/applications/new填写注册表单(如果你没有GitHub账户,那么需要先注册一个才能访问这个页面。),注册表单各个字段的作用和示例如图所示。

在GitHub注册OAuth程序

在GitHub注册OAuth程序

表单中的信息都可以后续进行修改。在开发时,程序的名称、主页和描述可以使用临时的占位内容。但Callback URL(回调URL)需要正确填写,这个回调URL用来在用户确认授权后重定向到程序中。因为我们需要在本地开发时进行测试,所以需要填写本地程序的URL,比如http://127.0.0.1:5000/callback/github,我们需要创建处理这个请求的视图函数,在这个视图函数中获取回调URL附加的信息,后面会详细介绍。

注意 这里因为是在开发时进行本地测试,所以填写了程序运行的地址,在生产环境要避免指定端口。另外,在这里localhost和127.0.0.1将会被视为两个地址。在程序部署上线时,你需要将这些地址更换为真实的网站域名地址。

注册成功后,我们会在重定向后的页面看到我们的Client ID(客户端ID)和Client Secret(客户端密钥),我们需要将这两个值分别赋值给配置变量GITHUB_CLIENT_ID和GITHUB_CLIENT_SECRET:

GITHUB_CLIENT_ID = 'GitHub客户端ID'
GITHUB_CLIENT_SECRET = 'GitHub客户端密钥'

注意 示例程序中为了便于测试,直接在脚本中写出了,在生产环境下,你应该将它们写入到环境变量,然后在脚本中从环境变量读取。

安装并初始化GitHub-Flask

首先使用pip或Pipenv等工具安装GitHub-Flask:

$ pip install github-flask

和其他扩展类似,你可以使用下面的方式初始化扩展(注意扩展类大小写):

from flask import Flask
from flask_github import GitHub
app = Flask(__name__)
github = GitHub(app)

如果你使用工厂函数创建程序,那么可以使用下面的方式初始化扩展:

from flask import Flask
from flask_github import GitHub
github = GitHub()
... 

def create_app():
    app = Flask(__name__)
    app.config.from_pyfile('settings.py')
    github.init_app(app)
    ...
    return app

注意 虽然扩展名称是GitHub-Flask,但实际的包名称仍然是flask_github(Flask扩展名称可以倒置(即“Foo-Flask”),但包名称的形式必须为“flask_foo“。)。另外要注意扩展类的拼写,其中H为大写。

进行OAuth授权

创建用户模型

在示例程序中,我们首先进行了下面的基础工作:

  • 定义基本配置
  • 创建一个简单的用户模型来存储用户信息(使用Flask-SQLAlchemy)
  • 实现登录和注销的管理功能(使用session实现,可以使用Flask-Login简化)
  • 创建用于初始化数据库的命令函数
app = Flask(__name__)

app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'secret string')
# Flask-SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///' + os.path.join(app.root_path, 'data.db'))
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)

# 命令函数
@app.cli.command()
@click.option('--drop', is_flag=True, help='Create after drop.')
def initdb(drop):
    """Initialize the database."""
    if drop:
        db.drop_all()
    db.create_all()
    click.echo('Initialized database.')

# 存储用户信息的数据库模型类
class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(100))  # 用户名
    access_token = db.Column(db.String(200))  # 授权完成后获取的访问令牌

# 管理每个请求的登录状态,如果已登录(session里有用户id值),将模型类对象保存到g对象中
@app.before_request
def before_request():
    g.user = None
    if 'user_id' in session:
        g.user = User.query.get(session['user_id'])

# 登入
@app.route('/login')
def login():
    if session.get('user_id', None) is None:
        ...  # 进行OAuth授权流程,具体见后面
    flash('Already logged in.')
    return redirect(url_for('index'))

# 登出
@app.route('/logout')
def logout():
    session.pop('user_id', None)
    flash('Goodbye.')
    return redirect(url_for('index'))

现在我们可以执行上面创建的initdb命令来创建数据库和表(确保当前目录在demos/github-login下):

$ flask initdb

创建登录按钮

我们在本节一开始详细描述了以GitHub为例的完整的OAuth授权的过程,现在让我们来创建登录按钮。示例程序非常简单,只包含一个主页(index.html),这个页面由index视图处理:

@app.route('/')
def index():
    is_login = True if g.user else False  # 判断用户登录状态
    return render_template('index.html', is_login=is_login)

这个视图在渲染模板时传入了用于判断用户登录状态的is_login变量,我们在模板中根据这个变量渲染不同的元素,如果已经登入,显示退出按钮,否则显示登入按钮:

{% if is_login %}
    <a class="btn" href="{{ url_for('logout') }}">Logout</a>
{% else %}
    <a class="btn" href="{{ url_for('login') }}">Login with GitHub</a>
{% endif %}

在实际的项目中,你可以使用GitHub的logo来让登录按钮更好看一些。

提示 使用Flask-Login时,你可以直接在模板中通过current_user.is_authenticated属性来判断用户登入状态。

发送授权请求

这个登录按钮的URL指向的是login视图,这个视图用来发送授权请求,如下所示:

@app.route('/login')
def login():
    if session.get('user_id', None) is None:  # 判断用户登录状态
        return github.authorize(scope='repo')
    flash('Already logged in.')
    return redirect(url_for('index'))

在这个视图中,如果用户没有登录,我们就调用github.authorize()方法。这个方法会生成授权URL,并向这个URL发送请求。

附注 GitHub-Flask扩内置了除了客户端ID和密钥外所有必要的URL,比如API的URL,获取访问令牌的URL等(我们也可以通过相应的配置键进行修改,具体参考GitHub-Flask的文档)。

发起认证请求的URL中必须加入的参数是客户端ID,GitHub-Flask会自动使用我们之前通过配置变量传入的值。在授权URL中附加的可选参数如下所示:

名称

类型

说明

scope

字符串

请求的权限列表,使用空格分隔

state

字符串

用于CSRF保护的随机字符,也就是CSRF令牌

redirect_uri

字符串

用户授权结束后的重定向URL(必须是外部URL)

这三个参数都可以在调用github.authorize()方法时使用对应的名称作为关键字参数传入。

如果不设置scope,GitHub-Flask扩展默认设置为None,那么会拥有的权限是获取用户的公开信息。但是因为我们需要测试为项目加星(star)的操作,所以需要请求名为repo的权限值。

附注 选择scope时尽量只选择需要的内容,申请太多的权限可能会被用户拒绝。GitHub提供的所有的可用scope列表及其说明可以在GitHub开发文档看到。

如果不设置redirect_uri,那么GitHub会使用我们填写的callback URL。但是需要注意的是,如果我们填写了,那就必须和注册程序时填写的URL完全相同。我们在这里没有指定,因此将会使用注册OAuth程序时设置的http://localhost:5000/callback/github

获取access令牌(访问令牌)

现在程序会重定向到GitHub的授权页面(会先要求登录GitHub),如下所示:

授权页面

授权页面

当用户同意授权或拒绝授权后,GitHub会将用户重定向到我们设置的callback URL,我们需要创建一个视图函数来处理回调请求。如果用户同意授权,GitHub会在重定向的请求中加入code参数,一个临时生成的值,用于程序再次发起请求交换access token。程序这时需要向请求访问令牌URL(即https://github.com/login/oauth/access_token)发起一个POST请求,附带客户端ID、客户端密钥、code以及可选的redirect_uri和state。请求成功后的的响应会包含访问令牌(Access Token)。

很幸运,上面的一系列工作GitHub-Flask会在背后替我们完成。我们只需要创建一个视图函数,定义正确的URL规则(这里的URL规则需要和GitHub上填写的Callback URL匹配),并为其附加一个github.authorized_handler装饰器。另外,这个函数要接受一个access_token参数,GitHub-Flask会在授权请求结束后通过这个参数传入访问令牌,如下所示:

@app.route('/callback/github')
@github.authorized_handler
def authorized(access_token):
    if access_token is None:
        flash('Login failed.')
        return redirect(url_for('index'))
    # 下面会进行创建新用户,保存访问令牌,登入用户等操作,具体见后面
    ...
    return redirect(url_for('chat.app'))

接受到GitHub返回的响应后,GitHub-Flask会调用这个authorized()函数,并传入access_token的值。如果授权失败,access_token的值会是None,这时我们重定向到主页页面,并显示一个错误消息。如果access_token不为None,我们会进行创建新用户,保存访问令牌,登入用户等操作,具体见下一节。

获取用户在GitHub上的资源

在获取到访问令牌后,我们需要做下面的工作:

  • 判断用户是否已经存在于数据库中,如果存在就登入用户,更新访问令牌值(因为access是有过期时间的)
  • 如果数据库中没有该用户,那么创建一个新的用户记录,传入对应的数据,最后登入用户

在这个示例程序中,我们使用用户名(username)作为用户的唯一标识,为了从数据库中查找对应的用户,我们需要获取用户在GitHub上的用户名。

如果授权成功,那么我们就使用这个访问令牌向GitHub提供的Web API的/user端点发起一次GET请求。这可以通过GitHub-Flask提供的get()方法实现,传入访问令牌作为access_token参数的值。我们把表示用户的资源端点“user”传入get()方法,因为GitHub-Flask会自动补全完整的请求URL,即https://api.github.com/user

response = github.get('user', access_token=access_token)

提示 GitHub-Flask提供了一系列方法来调用GitHub通过Web API开放的资源。和在jQuery为AJAX提供的方法类似,它提供了底层的request()方法和方便的get()、post()、put()、delete()等方法(这些方法内部会调用request方法),可以用来发送不同HTTP方法的请求。

/user端点对应用户资料,返回的JSON数据如下所示:

{
 "avatar_url": "https://avatars3.githubusercontent.com/u/12967000?v=4", 
 "bio": null, 
 "blog": "greyli.com", 
 "company": "None", 
 "created_at": "2015-06-19T13:00:23Z", 
 "email": "withlihui@gmail.com", 
 "events_url": "https://api.github.com/users/greyli/events{/privacy}", 
 "followers": 132, 
 "followers_url": "https://api.github.com/users/greyli/followers", 
 "following": 8, 
 "following_url": "https://api.github.com/users/greyli/following{/other_user}", 
 "gists_url": "https://api.github.com/users/greyli/gists{/gist_id}", 
 "gravatar_id": "", 
 "hireable": true, 
 "html_url": "https://github.com/greyli", 
 "id": 12967000, 
 "location": "China", 
 "login": "greyli", 
 "name": "Grey Li", 
 "node_id": "MDQ6VXNlcjEyOTY3MDAw", 
 "organizations_url": "https://api.github.com/users/greyli/orgs", 
 "public_gists": 7, 
 "public_repos": 61, 
 "received_events_url": "https://api.github.com/users/greyli/received_events", 
 "repos_url": "https://api.github.com/users/greyli/repos", 
 "site_admin": false, 
 "starred_url": "https://api.github.com/users/greyli/starred{/owner}{/repo}", 
 "subscriptions_url": "https://api.github.com/users/greyli/subscriptions", 
 "type": "User", 
 "updated_at": "2018-06-24T02:05:38Z", 
 "url": "https://api.github.com/users/greyli"
}

附注 用户端点返回的响应示例以及其他所有开放的资源端点可以在GitHub的API文档(https://developer.github.com/v3/)中看到。

GitHub-Flask会把GitHub的JSON响应主体解析为一个字典并返回,我们使用对应的键获取这些数据。其中登录用户名使用login作为键获取:

username = response['login']

获取到用户名后,我们判断是否已存在该用户,如果存在更新access_token字段值;如果不存在则创建一个新的User实例,把用户名和访问令牌存储到用户模型的对应字段里:

user = User.query.filter_by(username=username).first()
if user is None:
    user = User(username=username, access_token=access_token)
    db.session.add(user)
 user.access_token = access_token # update access token
 db.session.commit()

最后,我们登入对应的用户对象或是新创建的用户对象(将用户id写入session):

flash('Login success.')
# log the user in
# if you use flask-login, just call login_user() here.
session['user_id'] = user.id

因为我们需要在其他视图里调用GitHub资源,为了避免每次都获取和传入访问令牌,我们可以使用github.access_token_getter装饰器创建一个统一的令牌获取函数: 

@github.access_token_getter
def token_getter():
    user = g.user
    if user is not None:
       return user.access_token

当你在某处直接使用github.get()等方法而不传入访问令牌时,GitHub-Flask会通过你提供的这个回调函数来获取访问令牌。

注意 虽然在很多开源库的示例程序中,都会把access令牌存储到session中,但session不能用来存储敏感信息(具体可以访问专栏的这篇文章了解)。所以除了作测试用途,在生产环境下正确的做法是把访问令牌存储到数据库中。

现在,我们的主页视图需要更新,对于登录的用户,我们将会显示用户在GitHub上的资料:

@app.route('/')
def index():
    if g.user:
        is_login = True
        response = github.get('user')
        avatar = response['avatar_url']
        username = response['name']
        url = response['html_url']
        return render_template('index.html', is_login=is_login, avatar=avatar, username=username, url=url)
    is_login = False
    return render_template('index.html', is_login=is_login)

类似的,我们使用github.get()方法获取/user端点的用户资料,因为设置了令牌获取函数,所以不用显式的传入访问令牌值。这些数据(头像、显示用户名和GitHub用户主页URL)将会显示在主页,如下图所示:

登录成功后的主页

登录成功后的主页

因为我们在进行授权时请求了repo权限,我们还可以对用户的仓库进行各类操作,示例程序中添加了一个加星的示例,如果你登录后点击主页的“Star HelloFlask on GitHub”按钮,就会加星对应的仓库。这个按钮指向的star视图如下所示:

@app.route('/star/helloflask')
def star():
    github.put('user/starred/greyli/helloflask', headers={'Content-Length': '0'})
    flash('Star success.')
    return redirect(url_for('index'))

完整的用于处理回调请求的authorized()视图函数如下所示:

@app.route('/callback/github')
@github.authorized_handler
def authorized(access_token):
    if access_token is None:
        flash('Login failed.')
        return redirect(url_for('index'))

    response = github.get('user', access_token=access_token)
    username = response['login'] # get username
    user = User.query.filter_by(username=username).first()
    if user is None:
        user = User(username=username, access_token=access_token)
        db.session.add(user)
    user.access_token = access_token # update access token
    db.session.commit()
    flash('Login success.')
    # log the user in
    # if you use flask-login, just call login_user() here.
    session['user_id'] = user.id
    return redirect(url_for('index'))

走进现实

一次完整的OAuth认证就这样完成了。在实际的项目中,支持第三方登录后,我们需要对原有的登录系统进行调整。通过第三方认证创建的用户没有密码,所以如果这部分用户使用传统方式登录的话会出现错误。我们添加一个if判断,如果用户对象的password_hash字段为空时,我们会返回一个错误提示,提醒用户使用上次使用的第三方服务进行登录,如下所示:

@app.route('/login', methods=['GET', 'POST'])
def login():
    ...
    if request.method == 'POST':
        ...
        user = User.query.filter_by(email=email).first()

        if user is not None:
            if user.password_hash is None:
                flash('Please use the third patry service to log in.')
                return redirect(url_for('.login'))
        ...

如果你想让用户也可以直接使用账户密码登录,那么可以在授权成功后重定向到新的页面请求用户设置密码。

相关链接

《Flask Web开发实战》最新动态

《Flask Web开发实战:入门、进阶与原理解析》是我刚刚完成写作的一本技术书,涵盖了Flask Web开发学习的完整路径,而且包含大量的程序实例。你可以通过下面的文章了解这本书的更多信息:

本书动态:

  • 2017/3/1 开始写作
  • 2017/12/7 初稿完成
  • 2018/1/18 二稿完成
  • 2018/3/26 三稿完成
  • 2018/4/29 四稿完成
  • 2018/5/16 五稿完成
  • 2018/5/22 定稿(六稿)
  • 2018/6/5 确定最终修改,写作正式完结
  • 2018/6/20 完成封面设计初稿
  • 2018/6/22 完成封面文案初稿
  • 2018/6/22 确定英文书名为《Python Web Development with Flask》
  • 2018/8/20 下厂印刷
  • 2018/8/24 Kindle电子书上架(https://www.amazon.cn/dp/B07GST8Z8M
  • 2018/8/26 本书的豆瓣条目页面创建成功
  • 2018/8/28 电子书上架豆瓣阅读(read.douban.com/ebook/5
  • 2018/9/10 电商平台已经可以购买,亚马逊和京东自营预计9/15有货,访问本书主页查看购买链接

为Git仓库里的.idea文件夹正名

在网络上,我曾多次看到人们对于Git仓库中的.idea文件夹的偏见。最近的一次是在某个博客中技术专家对于志愿者提交的项目的点评,其中在“项目规范”中有一条加分项为“没有 .idea 这种不该上传的文件夹”;另一次是在知乎评价某程序员的问题下某个回答的评论中,有人发现该程序员的GitHub仓库里有.idea目录,就居高临下的将其作为理由讽刺该程序员,潜台词即“项目里有.idea = 水平低下”。想当然的,我没看到的类似情况会更多,而这些观点又会影响很多不熟悉具体事实的人,我想我应该尽一份力来改变这个错误的观点继续蔓延。

提示 尽管本文的标题使用了Git,本文的内容同样适用于其他VCS(Version Control System,版本控制系统)。

什么是.idea文件夹

当你使用JetBrains出品的IDE(Integrated Development Enviroment,集成开发环境)时,比如PyCharm、WebStorm或IntelliJ IDEA等,它们会在创建项目后在项目根目录创建一个.idea文件夹,其中保存了项目特定的配置文件。

至于为什么命名为.idea,则是因为IntelliJ IDEA是JetBrains最早推出的IDE(JetBrains一开始叫IntelliJ),因此使用IDEA作为配置文件夹的名称。按照这个SO回答里最高票答案的猜测,或许IntelliJ IDEA这个名字的含义是这样组成的:

  • Intelli ===> Intelligent
  • J ===> Java
  • Idea ===>IDE that is Advanced or Idea just means idea( I have a good idea …like this ) …

是否应该把.idea提交进Git仓库

这个问题没有唯一的答案,在Stack Overflow有很多类似的讨论。总的来说,开发者可以自由选择是否把IDE相关的配置文件(即.idea目录下的文件)提交到Git仓库中:

  • 如果你想让其他使用相同IDE的用户可以更方便(规范)的对项目进行开发,那么就把它提交到Git仓库中。
  • 如果你觉得Git仓库不应该包含和项目本身无关的文件,那么也可以不将它提交到Git仓库中。

正确的提交方法

当然,将.idea目录整个提交到Git仓库的行为并不可取。因为.idea目录下的文件中有包含隐私的内容,比如你的文件操作变动、用户词典、系统环境变量、数据库密码等等,这些文件对项目其他潜在的参与者没有用处,而且会泄露你的隐私。

按照JetBrains官方的建议,在使用VCS时我们应该遵循下面的原则:

分享下面的文件:

  • 除了workspace.xml、usage.statistics.xml和tasks.xml以外.idea目录下的所有文件
  • 所有可以被在不同模块目录下定位到的.iml模块文件(适用于IntelliJ IDEA)

谨慎分享下面的文件

  • Android artifacts that produce a signed build,因为它们包含keystore密码(前半句不理解,暂时保留原文)
  • 在IntelliJ IDEA 13 和之前的版本中的dataSources.ids和datasources.xml文件,它们包含数据库密码

避免分享下面的文件:

  • 对于使用Gradle或Maven的项目,避免分享.iml和.idea/modules.xml文件,因为它们会在导入时生成
  • gradle.xml文件
  • 用户字典(dictionaries文件夹)
  • .idea/libraries目录下的XML文件,因为它们会从Gradle或Maven项目中生成

另外,对于旧的项目格式(.ipr/.iml/.iws files)来说:

  • 分享项目.ipr文件和所有的.iml模块文件,不要分享.iws文件,因为它存储用户特定设置。

对于Git,你可以参考GitHub提供的JetBrains适用的.gitignore模板

我的新书中包含多个Flask项目,这些项目中的.gitignore文件则是通过gitignore.io来生成的。你可以在gitignore.io主页的输入框中输入你使用的操作系统、编程语言和IDE,它会快速为你来生成一份适用这些语言和平台的.gitignore规则,比如这个是输入Python+PyCharm生成的模板。你可以在这个模板的基础上添加自定义规则。

总结

如果你不想在Git仓库中提交IDE相关的配置文件,那么你可以忽略.idea文件夹;相反,你也可以有选择的把.idea目录下的文件提交进Git仓库。也就是说,项目Git仓库中是否包含.idea文件夹与程序员的开发水平并没有直接关系。

Pipenv:新一代Python项目依赖管理工具

UPDATE(2019/8/31):不要用 Pipenv

什么是Pipenv

Pipenv是Kenneth Reitz在2017年1月发布的Python依赖管理工具,现在由PyPA维护。你可以把它看做是pip和virtualenv的组合体,而它基于的Pipfile则用来替代旧的依赖记录方式(requirements.txt)。
 
在这篇文章里,我将会以旧的依赖管理工作流程作为对比来介绍Pipenv的基本用法,更详细的用法可以参考Pipenv文档,或是Kenneth Reitz在PyCon 2018的演讲《Pipenv: The Future of Python Dependency Management》
 
顺便说一句,我的还没想好名字的Flask书中所有示例程序都使用了Pipenv进行依赖管理。
提示 如果你对virtualenv的用法以及虚拟环境的概念不熟悉的话,可以通过专栏的旧文《Flask出发之旅》进行简单的认识。

为什么使用Pipenv

Pipenv会自动帮你管理虚拟环境和依赖文件,并且提供了一系列命令和选项来帮助你实现各种依赖和环境管理相关的操作。简而言之,它更方便、完善和安全。你可以通过Pipenv文档开头的介绍来了解它的详细特性。Pipenv的slogan是“Python Dev Workflow for Humans”,作为人类,当然应该尝试一下……

如何使用Pipenv

假设我们要编写一个博客程序,项目的依赖是Flask和Flask-WTF。顺便说一句,可以使用下面的命令安装Pipenv:
$ pip install pipenv
下面我会通过不同操作来给出所需命令的对比,OLD(旧)表示使用pip和virtualenv,NEW(新)表示使用Pipenv。
 

创建虚拟环境

  • OLD
$ virtualenv venv 
提示 这里的venv是虚拟环境目录的名称,你可以自由更改,这会在你的项目根目录创建一个venv文件夹,其中包含独立的Python解释器环境。
  • NEW
$ pipenv install
Pipenv会自动为你创建虚拟环境,自动生成一个随机的虚拟环境目录名。
 

激活虚拟环境

  • OLD
在Linux或macOS系统中:
$ . venv/bin/activate
Windows:
> venv\Scripts\activate
  • NEW
$ pipenv shell
此外,Pipenv还提供了一个pipenv run命令,在该命令后附加的参数会直接作为命令在虚拟环境中执行,这允许你不必显式的激活虚拟环境即可在虚拟环境中执行命令。比如,pipenv run python会启动虚拟环境中的Python解释器。
 

安装依赖到虚拟环境

  • OLD
$ . venv/bin/activate # 需要先激活虚拟环境
(venv)$ pip install flask flask-wtf 
  • NEW
使用Pipenv,不管你是否激活了虚拟环境,都可以通过pipenv install命令安装:
$ pipenv install flask flask-wtf 
事实上,对一个新项目来说,你不必手动使用pipenv install来创建虚拟环境。当使用pipenv install xxx直接安装依赖包时,如果当前目录不包含虚拟环境,Pipenv会自动创建一个。
 

记录依赖

  • OLD
(venv)$ pip freeze > requirements.txt
这个命令会把依赖列表写入requirements.txt文件。每当你安装或卸载了依赖包时,都需要手动更新这个文件。你必须保持谨慎,否则非常容易把依赖列表弄乱。
 
  • NEW
使用Pipenv时,什么都不必做,Pipenv会自动帮你管理依赖。Pipenv会在你创建虚拟环境时自动创建Pipfile和Pipfile.lock文件(如果不存在),并且会在你使用pipenv install和pipenv uninstall命令安装和卸载包时自动更新Pipfile和Pipfile.lock。
附注 Pipfile用来记录项目依赖包列表,而Pipfile.lock记录了固定版本的详细依赖包列表。

在部署环境安装依赖

  • OLD
当我们需要在一个新的环境,比如部署上线环境安装所有依赖时,我们需要重复上面的多条命令:
$ virtualenv venv # 创建虚拟环境
$ . venv/bin/activate # 激活虚拟环境
(venv)$ pip install -r requirements.txt # 安装requirement.txt中记录的依赖
  • NEW
使用Pipenv则只需要执行pipenv install,它会自动安装Pipfile中记录的依赖:
$ pipenv install

区分开发依赖

  • OLD
使用requirements.txt时,我们通过会单独创建一个requirements-dev.txt文件来手动加入开发依赖。比如项目开发时才会用到pytest,那么你需要手动创建这个文件,然后写入:
-r requirements.txt
pytest==1.2.3
在新的开发环境安装依赖时,你需要安装这个文件中的依赖:
(venv)$ pip install -r requirements-dev.txt
  • NEW
使用Pipenv时,你只需要在安装pytest时添加一个–dev选项,它会自动被分类为开发依赖(写入Pipfile的dev-packages一节中):
$ pipenv install pytest --dev
在新的开发环境安装依赖时,也只需要在pipenv install命令后添加–dev选项即可一并安装开发依赖:
$ pipenv install --dev

总结

为了让你更轻松的过渡,Pipenv甚至提供了读取和生成requirements.txt文件的功能(在使用pipenv install命令时它会自动读取requirements.txt文件安装依赖并生成Pipfile文件)。希望这篇文章可以让你更快的上手Pipenv。
 
– – – – –
 
 
 

新发布的Flask1.0带来了哪些新变化?

就在五个小时前,Flask终于发布了8年来的第一个主版本——1.0,这个版本带来了很多新的变化,让我们拥抱变化吧,请使用下面的命令来更新:

$ pip install -U flask

主要的变化

  • 从0.11开始,Flask引入了命令行支持,建议使用flask run命令来取代app.run()方法。1.0版本支持将Flask_APP环境变量设为包名称。Flask会自动在app.pywsgi.py寻找名称为appapplication的程序实例,同时会在传入的模块或包中寻找名称为create_app()make_app()的0参数的工厂函数。
  • 新添加了一个FLASK_ENV环境变量来设置Flask运行的环境,默认为production。在开发时需要设为development,这会自动开启调试器和重载器(即调试模式),避免直接使用FLASK_DEBUG
  • 当安装了python-dotenv时,flask命令(比如flask runflask shell等)会自动从项目根目录下的.flaskenv.env文件中导入环境变量,这样可以避免每次都手动设置环境变量。前者用来存储公开的Flask相关的环境变量,比如FLASK_APPFLASK_ENV等,后者用来存储包含敏感信息的环境变量,比如邮箱密码,API密钥等等,需要将文件名添加到.gitignore中。
  • 开发服务器默认开启多线程支持。
  • 日志系统被极大的简化,日志器总是命名为flask.app,它只会在没有日志处理器注册的情况下才添加处理器,而且不会移除已经存在的处理器。
  • 测试客户端(test_client)支持使用json参数来传入JSON数据,你可以使用这个特性来测试Web API。返回的响应对象添加了get_json方法来获取JSON数据。
  • 添加了一个 test_cli_runner() 方法,它可以用来触发使用Click注册的flask命令,我们可以使用它来测试自定义的flask命令。
  • 添加了一个flask routes命令,用来输出程序所有注册的路由。
  • 移除了旧的扩展导入代码flask.ext,还在使用旧的导入方式的扩展将无法使用。
  • 带来了一个重写后的Flaskr教程,包含了大量重构和改进,其他部分的文档也包含大量更新。
  • 和大多数项目一样,1.0版本不再支持Python2.6和3.3。

你可以访问Flask Changelog – Flask 1.0 documentation查看完整的Changelog,并了解每一个变化所对应的PR或Issue。

顺便说一句

正在写的Flask书将在五月初结束写作,不过距离上市还需要一小段时间(按照正常的出版流程,大概是40天~三个月区间内)。这本书的内容完全采用1.0版本的Flask,其他库也都使用最新版,包括Bootstrap等前端库。另外,专栏之前发布的文章相关的示例程序已经过时,请不要参考,可以阅读Flask文档中更新的Flaskr教程。如果不着急的话,可以等一等,新书会带来几个更完善的示例程序。

Flask-CKEditor:为Flask项目集成富文本编辑器

这篇文章属于“Flask常用扩展介绍系列”,这个系列的文章目录索引可以在《Flask常用扩展介绍系列文章索引》看到。
 

富文本编辑器即WYSIWYG(What You See Is What You Get)编辑器(所见即所得编辑器)。在Web程序中可用的开源富文本编辑器中,CKEditor是一个流行的选择。Flask-CKEditor简化了将CKEditor集成到Flask项目中的过程,可以让你方便的在Flask项目中添加富文本编辑器。它包含下面这些特性:

  • 提供WTForms/Flask-WTF集成支持
  • 支持图片上传与插入
  • 通过Flask配置来设置编辑器的语言、高度等参数
  • 支持代码块语法高亮

《Flask Web开发实战》中的第2个示例程序(博客程序Bluelog)使用了这个扩展。

基本用法

安装

首先使用pip或Pipenv等工具安装:

$ pip install flask-ckeditor

初始化扩展

一般情况下,你只需要导入并实例化CKEditor类,并传入程序实例即可:

from flask_ckeditor import CKEditor

app = Flask(__name__)
ckeditor = CKEditor(app)

如果你使用了工厂函数,那么也可以调用init_app()方法来进行初始化:

from flask_ckeditor import CKEditor

ckeditor = CKEditor()

def create_app():
    app = Flask(__name__)
    ckeditor.init_app(app)
    return app

引入CKEditor资源

为了使用CKEditor,我们首先要在模板中引入CKEditor的JavaScript等资源文件。推荐的做法是自己编写资源引用语句,你可以在CKEditor提供的Online Builder构建一个自定义的资源包,下载解压后放到项目的static目录下, 并引入资源包内的ckeditor.js文件,比如(实际路径按需调整):

<script src="{{ url_for('static', filename='ckeditor/ckeditor.js') }}"></script>

如果你不需要自定义,那么也可以从CDN加载:

<script src="//cdn.ckeditor.com/4.9.2/standard/ckeditor.js"></script>

最后,作为替代选项,你也可以使用Flask-CKEditor提供的ckeditor.load()方法来生成引用语句:

{{ ckeditor.load() }}

它默认从CDN加载资源,将配置变量CKEDITOR_SERVE_LOCAL设为True会使用扩展内置的本地资源。另外,你也可以使用custom_url参数来使用自定义资源包:

{{ ckeditor.load(custom_url=url_for('static', filename='ckeditor/ckeditor.js')) }}

创建CKEditor文本区域

Flask-CKEditor提供了两种方式来CKEditor文本区域:

1. 与WTForms/Flask-WTF集成

Flask-CKEditor提供了一个CKEditorField字段类,和你平时从WTForms导入的StringField、SubmitField用法相同。事实上,它就是对WTForms提供的TextAreaField进行了包装。

作为示例,我们可以创建一个写文章的表单类。这个表单类包含一个标题字段(StringField),一个正文字段(CKEditorField)和一个提交字段(SubmitField)。你会看到,其中的正文字段使用了CKEditorField。

from flask_wtf import FlaskForm
from flask_ckeditor import CKEditorField
from wtforms import StringField, SubmitField

class PostForm(FlaskForm):
    title = StringField('Title')
    body = CKEditorField('Body')
    submit = SubmitField('Submit')

 

在渲染文本编辑区域的模板中,我们可以像往常一样渲染表单:

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

{{ ckeditor.load() }}
{{ ckeditor.config(name='body') }}

唯一需要注意的是,我们需要在资源引用语句后调用ckeditor.config()方法来让对CKEditor进行配置和初始化,并将name参数的值设为CKEditor字段的属性名,这里即body。

当表单提交后,你可以像其他字段一样通过form.attr.data属性来获取数据,这里的文本区域数据即form.body.data。

2. 手动创建

如果你不使用WTForms/Flask-WTF,那么可以直接使用Flask-CKEditor提供的ckeditor.create()方法在模板中创建文本编辑区域:

<form method="post">
    {{ ckeditor.create() }}
    <input type="submit">
</form>

{{ ckeditor.load() }}
{{ ckeditor.config() }}  <!-- 这时不用设置name参数 -->

在表单被提交后,你可以使用ckeditor作为键从表单数据中获取对应的值,即request.form.get('ckeditor')。

提示 完整的示例程序在examples/basic/examples/without-flask-wtf目录下。

配置变量

Flask-CKEditor提供了下面这些配置变量:

配置 默认值 说明
CKEDITOR_SERVE_LOCAL False 使用内置的ckeditor.load()方法时,设置是否使用本地资源,默认从CDN加载
CKEDITOR_PKG_TYPE 'standard' CKEditor资源包的类型,basicstandardfull中的一个
CKEDITOR_LANGUAGE None 设置CKEditor文本编辑器的语言,默认会自动探测用户浏览器语言,所以一般不需要设置。你也可以设置ISO 639格式的语言码,比如zhenjp等
CKEDITOR_HEIGHT CKEditor默认 编辑器高度,单位为px
CKEDITOR_WIDTH CKEditor默认 编辑器宽度,单位为px
CKEDITOR_FILE_UPLOADER None 处理上传文件的URL或端点
CKEDITOR_FILE_BROWSER None 处理文件浏览的URL或端点
CKEDITOR_ENABLE_MARKDOWN False 设置是否开启markdown插件,需要安装对应插件
CKEDITOR_ENABLE_CODESNIPPET False 设置是否开启codesnippet插件(插入代码块),需要安装对应插件
CKEDITOR_CODE_THEME 'monokai_sublime' 当使用codesnippet插件时,设置语法高亮的主题
CKEDITOR_EXTRA_PLUGINS [] 在CKEditor中开启的额外扩展列表,对应的扩展需要被安装

图片上传

在使用文本编辑器写文章时,上传图片是一个很常见的需求。在CKEditor中,图片上传可以通过File Browser插件实现。在服务器端的Flask程序中,你需要做三件事:

  1. 创建一个视图函数来处理并保存上传文件
  2. 创建一个视图函数来获取图片文件,类似Flask内置的static端点
  3. 将配置变量CKEDITOR_FILE_UPLOADER设为这个视图函数的URL或端点值

完整的代码示例如下所示:

from flask_ckeditor import upload_success, upload_fail

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'

@app.route('/files/<path:filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
def upload():
    f = request.files.get('upload')  # 获取上传图片文件对象
    # Add more validations here
    extension = f.filename.split('.')[1].lower()
    if extension not in ['jpg', 'gif', 'png', 'jpeg']:  # 验证文件类型示例
        return upload_fail(message='Image only!')  # 返回upload_fail调用
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return upload_success(url=url) # 返回upload_success调用

注意 传入request.files.get()的键必须为'upload', 这是CKEditor定义的上传字段name值。

在处理上传文件的视图函数中,你必须返回upload_success()调用,每将url参数设置为获取上传文件的URL。通常情况下,除了保存文件,你还需要对上传的图片进行验证和处理(大小、格式、文件名处理等等,具体可以访问这篇《Flask文件上传(一):原生实现》了解),在验证未通过时,你需要返回upload_fail()调用,并使用message参数传入错误消息。

当设置了CKEDITOR_FILE_UPLOADER配置变量后,你可以在编辑区域点开图片按钮打开的弹窗中看到一个新的上传标签。另外,你也可以直接将图片文件拖拽到编辑区域进行上传,或复制文件并粘贴到文本区域进行上传(CKEditor >= 4.5)。

提示 对应的示例程序在examples/image-upload/目录下。

如果你使用的 CKEditor 版本小于 4.5,则使用下面的方式实现:

from flask import send_from_directory

app.config['CKEDITOR_FILE_UPLOADER'] = 'upload'  # this value can be endpoint or url

@app.route('/files/<filename>')
def uploaded_files(filename):
    path = '/the/uploaded/directory'
    return send_from_directory(path, filename)

@app.route('/upload', methods=['POST'])
@ckeditor.uploader
def upload():
    f = request.files.get('upload')
    f.save(os.path.join('/the/uploaded/directory', f.filename))
    url = url_for('uploaded_files', filename=f.filename)
    return url

为图片上传请求添加 CSRF 保护

如果你想为图片上传的请求添加 CSRF 保护,可以通过 CSRFProtect 实现(Flask-WTF 内置),首先安装 Flask-WTF:

$ pip install flask-wtf

然后初始化扩展:

from flask_wtf import CSRFProtect

csrf = CSRFProtect(app) 

Flask-CKEditor 0.4.3 版本内置了对 CSRFProtect 的支持,当使用 CSRFProtect 时,只需要把配置变量 `CKEDITOR_ENABLE_CSRF` 设为 `True` 即可开启 CSRF 保护:

app.config['CKEDITOR_ENABLE_CSRF'] = True

顺便说一句,在 Flask-CKEditor 内部需要把 CSRF 令牌放到上传图片的 AJAX 请求首部,这通过 CKEditor 4.9.0 版本新添加的一个配置选项 fileTools_requestHeaders 实现,这个配置可以用来想文件上传请求插入自定义的首部字段 。所以,如果想要实现 CSRF 保护,CKEditor 的版本需要大于或等于 4.9.0。

代码语法高亮

代码语法高亮可以通过Code Snippet插件实现(基于hightlight.js),你可以将配置变量CKEDITOR_ENABLE_CODESNIPPET设为Ture来开启。在此之前,你需要确保安装了这个插件(内置的资源包包含了这个插件)。

为了正确渲染代码块,你还需要引入对应的资源文件,最简单的方式是使用Flask-CKEditor提供的ckeditor.load_code_theme()方法:

<head>
 ...
 {{ ckeditor.load_code_theme() }}
</head>

你可以通过配置变量CKEDITOR_CODE_THEME来设置语法高亮的主题,默认为monokai_sublime,你可以在这个页面看到所有可用的主题对应的字符串。

提示 对应的示例程序在examples/codesnippet/目录下。

使用示例程序

项目仓库中提供了5个示例程序,分别展示基本用法、图片上传插入、代码语法高亮、Markdown模式和不使用Flask-WTF/WTForms。以基本示例程序为例,你可以通过下面的命令来获取并运行它:

$ git clone https://github.com/greyli/flask-ckeditor
$ cd flask-ckeditor/examples
$ pip install -r requirements.txt
$ python basic/app.py

然后在浏览器访问http://127.0.0.1:5000。

另外,helloflask仓库里在demos/form目录下的示例程序也包含一个Flask-CKEditor使用示例。

相关链接