分类 Web 下的文章

这是今年coding生涯里最开心的一件事:给最爱用的框架贡献了几行代码,虽然微不足道,但是非常有幸福感,也是特意记下来,把喜悦分享给有缘人~

缘起

一直以来想要突破花瓣和Pinterest对收图的限制,所以最近几天一直在折腾自己部署一个私有的花瓣网类似物,发现了一个叫Pinry的项目,但是作者也比较久没有更新了,feature上不是很完善,所以这几天一直在给这个项目发Pull Request.

在做PR的过程中,发现在采集图片的的过程中,保存WebP格式的文件会出错,而且错误来自django框架而不是 Pinry 项目本身,DjangoImageField 在保存图片的过程中,会尝试自动探测图片的尺寸。

为了提升效率,Django会尝试将图片文件的前面部分数据喂到 Pillow 里然后尝试解析出文件的元数据。

因为大部分图片源文件只要读取头部的部分数据就能获取到信息,所以这个方法通常都能工作得非常好,但是对于少部分图片,header 的 size 非常大,或者是分chunk的图片,或者流式的数据,需要读取很多数据才能获得完整的meta信息。

Django 的 ImageField 其实处理了这类问题,但是在捕获错误的时候,有一个 edge-case 没考虑到,就是读取WebP文件的时候,Pillow 会产生一个 RuntimeError ,这个未捕获的错误会导致Django产生内部错误,具体请参考:#29705 RuntimeError while saving webp file to ImageField

流程

提交Ticket前的准备

第一次给Django提交Ticket,首先跑去阅读了,啥都不懂,不过根据之前的经验,大概要考虑以下几个问题:

  • 怎么创建Ticket(Issue),怎么描述问题
  • 创建一个脚本或者可用的案例,来稳定的复现Bug
  • 创建一个Patch,并提交到相应的项目(Django使用Github跟踪和Review)

怎么创建Ticket

赶紧去阅读了一下Django提供的文档: 参见 Contribute to Django,有几个要点:

  • Reporting bugs and <strike>requesting features</strike>(因为是提交Bug,所以忽略后半)

    • 要点主要是描述Bug发现的 软件硬件环境, 复现方法样例脚本(可选),Bug 出现的 branch 可以在 Trac 系统内选择,要注意的是安全问题需要单独到另一个地方提交。
    • Django Trac 提供的选项非常多,基本上涵盖了整个bug修复流程中的所有中间过程和对bug提出者,修复者的要求。
    • 如果要自己修复,把bug assigin给自己即可

我创建了一个项目用来复现这个bug,并集成了travis-ci用来线上执行这个复现测试

实际上这个东西可以没有,可以只是简单描述复现条件和复现用的脚本,直接写到Issue里就好。

创建Patch

因为之前没给Django贡献过代码,所以赶紧去看了一下 Django Github Pull Request, 从这里就能看到他们的Pull Request标题是什么格式的,这个格式关联到他们的 CI , 所以非常重要,可以用来自动关闭 Ticket 。

  • 关于DjangoProject的 编码风格,可以参考先前提到的 Contribution Guide
  • 代码写好后,要本地跑一遍单元测试,这种Bug需要增加一个回归测试,用于测试以后的更改是否会导致这个Bug再次出现。

我最后的 Pull Request

Review 和 Merge

Django社区的效率非常高,真的非常惊喜,当天就完成了Review和Merge到Master的流程。

中间也得到了很多帮助和修改意见,详情可以参看 Pull Request。

结语

感慨Django社区的效率,第一次给一个非常活跃的项目贡献代码,也是达成了新成就,虽然只是个小Bug,不过非常开心~
希望这个流水账能帮到想要给项目贡献代码的朋友~

PS: 最近也在积极给 Pinry 贡献代码,希望能有更多的给 创作者 们使用的项目,能给 设计师/画师/写作者 们带来更多方便和自由和更高的生产力,也是我今后最想做的事情:)

其他创作者相关的工具:

  • PickTrue 图册下载器,最近新弄的项目,用来给 画师/设计师 们构建 Visual Library 目前支持Artstation, Pixiv,花瓣网。
  • Pinry - 个人版的花瓣(Pinterest) 可以不受限制(比如NFSW,版权)的限制收图,当然记得尊重版权,收图可以,不要到处传播哦。
  • TabArtstation - Chrome插件 , 在Chrome新标签中查看Artstation每日更新,收图必备,适合没时间去专门刷Artstation的同学
  • huaban-exporter - 花瓣导出器, 很早之前做的花瓣备份工具,会连同花瓣的元数据(比如描述,图片源链接)一起下载到本地,如果没有元数据需求,可以直接使用PickTrue

简化并修改view_config装饰器

因为Pyramid自带的view_config装饰器非常复杂,需要配置的参数比较多,修改一下装饰器,每次可以少写一点代码……

def get_with_permission(**settings):
    # delete debug tag TODO
    view_configer = view_config(decorator=permission_require(login_url=LOGIN_URL), **settings)
    return view_configer
 
 
def p_get(route_name, renderer='json', **kwargs):
        return get_with_permission(route_name=route_name, renderer=renderer, request_method="GET", **kwargs)

这样就可以将复杂的装饰器包裹在这个新装饰器内部,也可以简单的定制,只需要输入route_name就可以

login_required decorator and more

def permission_require(view_callable=None, login_url=None):
    """
    Return a login_required decorator with login_url redirection.
    Usage: @view_config(decorator=login_required, **configs) or @view_config(decorator=login_required(login_url='/'), **configs)
    :param view_callable: pyramid view_callable
    :param login_url: login url setting
    :type login_url: str or unicode
    :return: callable
    """
    if not view_callable and login_url:
        if not isinstance(login_url, (str, unicode)):
            raise TypeError('login_url must be a str/unicode object!')
        return lambda func: permission_check_actual(func, login_url)
    else:
        return permission_check_actual(view_callable, '/accounts/login')

以上是使用Pyramid自带的decorator特性定制的LoginRequired装饰器。

这里能获取到的函数参数和自写的装饰器是不同的,因为这里你可以取到request对象和context对象,但位于view_config外部的装饰器则很麻烦。

和外部装饰器搭配使用,可以获得更好的效果,可以在装饰器内部做登录和针对request对象或者request对象包含的任意成员的鉴权

SqlAlchemy 类方法。

class OperaionLog(Base):
    __tablename__ = 'operation_log'
 
    id = Column(Integer, primary_key=True, autoincrement=True)
    uid = Column(Integer, nullable=False)
    username = Column(Text)
    operation = Column(Text, nullable=False, doc=u'操作内容')
    result = Column(Text, doc=u'操作结果')
    ctime = Column(DateTime, nullable=False, default=datetime.datetime.utcnow)
 
    @classmethod
    def log_me(cls, uid, username, operation, result):
        with transaction.manager:
            log = cls(
                uid=uid,
                username=username,
                operation=operation,
                result=result,
            )
            dbsession.add(log)
            dbsession.flush()
        return True
 
    @classmethod
    def get_many(cls, start, end, is_dict=True):
        if is_dict:
            dbsession.query(cls).order_by(desc(cls.ctime))

以上代码描述了一个SQLAlchemy类方法,将对ORM的操作全部封装在ORM类当中,可以极大程度的精简查询代码,不用每个视图都写一次,且可以保证多处的查询获得一致的结果(比如开发中对于数据库字段状态的判断,不同开发时期很容易造成查询条件不一致)

自动数据库字段转换

虽然有很多项现成的Alchenmy2form这种类型的库,但对于输出数据或者对于要求输出JSON数据的情况下,使用REST-FRAMEWORK会比较重,所以使用一个自动格式化的工具会极大较少工作量又不用使用巨大的框架

class DBFieldConverter(object):
    """
    Convert specified SQLAlchemy model to output dict or other format.
    """
    _registry = {}
 
    def __init__(self, model_instance, allow_output=[], registry={}, extra_out=[]):
        """
        If registry is given, same method in this registry will overwrite that in class _registry.
        :param model_instance:
            SQLAlchemy model instance, inherit from declarative_base().
        :type registry: dict
        :type allow_output: list or tuple
        """
 
        if not hasattr(model_instance, '__table__'):
            raise TypeError("model_instance must be instance of sqlalchemy's model")
        if not isinstance(allow_output, (list, tuple)):
            raise TypeError("allowed outputs [{allow_output}] must be list or tuple")
        if not isinstance(registry, dict):
            raise TypeError('argument registry `{registry}` must be a dict.'.format(registry=registry))
        self.registry = registry
 
        self.model = model_instance
        self._allows = allow_output
        self.dict = dict((col, self._convert(getattr(self.model, col), type(self.model.__table__.columns[col].type))) for col in self.model.__table__.columns.keys())
        for key in extra_out:
            self.dict[key] = getattr(model_instance, key)
 
    def as_dict(self, pure=False):
        if pure:
            return self.dict
        else:
            return dict((item for item in self.dict.items() if item[0] in self._allows))
 
    def as_list(self, pure=False):
        if pure:
            return self.dict.items()
        else:
            return (item for item in self.dict.items() if item[0] in self._allows)
 
 
    @classmethod
    def register(cls, data_type, convert_method=None):
        """
        Register a field type converter to FieldConverter.
        Pass convert_method param or use default convert_method.
        If the data_type existed, this will return default
        :type data_type: wtforms.fields.Field
        :type convert_method: callable
        :rtype :bool
        :return True if register successfully, else False.
        """
        if not isinstance(data_type, type):
            raise TypeError('{data_type} must be instance of type'.format(data_type=data_type))
        if convert_method is not None:
            if not callable(convert_method):
                raise TypeError('{convert_method} must be callable'.format(convert_method=convert_method))
        else:
            convert_method = lambda field: field
        cls._registry[data_type] = convert_method
        return True
 
    @classmethod
    def unregister(cls, data_type):
        """
        Unregister a data-type converter fucntion.
        :type data_type: type
        :rtype bool
        :return True if register successfully
        """
        if not isinstance(data_type, type):
            raise TypeError('{data_type} must be instance of type'.format(data_type=data_type))
        if cls._registry.get(data_type, None) is None:
            logging.warning("Type {data_type} does not exist in registry.".format(data_type=data_type))
        else:
            del cls._registry[data_type]
            return True
 
    def _convert(self, data, data_type):
        if data is None:
            return None
        if self.registry.get(data_type) is not None:
            return self.registry[data_type](data)
        elif self._registry.get(data_type) is not None:
            return self._registry[data_type](data)
        else:
            logging.warning('{type} not contained in registry, return its default value'.format(type=data_type))
            return data
 
DBFieldConverter.register(ARRAY, _list2unicode) #自定义的转换函数
DBFieldConverter.register(JSON, json.dumps)
DBFieldConverter.register(Text)
DBFieldConverter.register(String)
DBFieldConverter.register(DateTime, datetime2utmp)  #自定义的转换函数
DBFieldConverter.register(Integer)
DBFieldConverter.register(CHAR)
DBFieldConverter.register(Boolean)
DBFieldConverter.register(UUID)
DBFieldConverter.register(INET)

以上代码使用白名单机制输出数据,指定输出的字段名即可,也可以使用pure=True,方便调试.当然也可以在运行时注册新的处理函数,对每个实例单独定制不同的转换函数(以适应不同的输出数据格式需求)

Class-Based view auto-init in init method.(Get safe GET paramters)

AttrDict with dict operation.

Validation by your own validator with wtforms.

Naming style in web development.