Uliweb 文档¶
介绍:
Uliweb 简介¶
它是什么¶
Uliweb是一个新的Python Web Framework,它之所以会产生是因为现有的框架多少有些令人 不满意的地方,而且许多情况下这些不满意的地方或多或少对我开发Web有影响,因此在经过对 不少框架的学习之后,我决定开发一个新的框架,希望可以综合我认为其它框架中尽可能多的 优点,同时使这个新的框架尽可能的简单,易于上手和使用。
这个项目是由Limodou <limodou@gmail.com>发起并创建的。其中得到了许多人的帮助。
License¶
Uliweb按照BSD进行发布。
基础组件¶
它并不完全是从头写的一个东西,我目前使用了一些库,如:
- Werkzeug 用它来进行框架的核心处理,比如:命令行, URL Mapping,Debug等。
- SqlAlchemy 基于它封装了Uliorm,可以使用ORM对 数据库进行处理。
还有一些比较小的引用,如:
另外还有一些是自已新造的,如:
- Form处理,可以用来生成HTML代码和对上传的数据进行校验
- i18n处理,包括对模板的处理
- Uliorm,是在SqlAlchemy基础之上进行的封装,同时参考了GAE中的datastore的代码
- 框架的处理代码,这块不可能不自已造了
- 插件机制,从Ulipad中移植并进行了改造
功能特点¶
组织管理
- 采用MVT模型开发。
- 分散开发统一管理。采用App方式的项目组织。每个App有自已的配置文件,templates目录, static目录。使得Uliweb的App重用非常方便。同时在使用上却可以将所有App看成一个整体, 可以相互引用静态文件和模板。缺省是所有App都是生效的,也可以指定哪些App是生效的。 所有生效App的配置文件在启动时会统一进行处理,最终合成一个完整的配置视图。
URL处理
灵活强大的URL映射。采用Werkzeug的Routing模块,可以非常方便地定义URL,并与View函数 进行绑定。同时可以根据view函数反向生成URL。支持URL参数定义,支持缺省URL定义,如:
appname/view_module/function_name
View与Template
- View模板的自动套用。当view返回dict对象时,自动根据view函数的名字查找对应的模板。
- 目前View方法支持一般函数和类的方式。
- 环境方式运行。每个view函数在运行时会处于一个环境下,因此你不必写许多的import,许多 对象可以直接使用,比如request, response等。可以大大减少代码量。
- 模板中可以直接嵌入Python代码,不需要考虑缩近,只要在块结束时使用pass。支持模板的 include和继承。
ORM
- 类Django和GAE的datastore,可以支持自动建表,同时提供命令行工具进行数据的备分,装入,建表等处理。
- 提供多数据库连接的支持
- 支持alembic的数据库迁移处理
i18n
- 支持代码和模板中的i18n处理
- 支持浏览器语言和cookie的自动选择,动态切换语言
- 提供命令行工具可以自动提取po文件,可以以App为单位或整个项目为单位。并在处理时自动将 所有语言文件进行合并处理。当发生修改时,再次提取可以自动进行合并。
扩展
- plugin扩展。这是一种插件处理机制。Uliweb已经预设了一些调用点,这些调用点会在特殊的地方 被执行。你可以针对这些调用点编写相应的处理,并且将其放在settings.py中,当Uliweb在启动 时会自动对其进行采集,当程序运行到调用点位置时,自动调用对应的插件函数。
- middleware扩展。它与Django的机制完全类似。你可以在配置文件中配置middleware类。每个 middleware可以处理请求和响应对象。
- views模块的初始化处理。在views模块中,如果你写了一个名为__begin__的函数,它将在执行 要处理的view函数之前被处理,它相当于一个入口。因此你可以在这里面做一些模块级别的处理, 比如检查用户的权限。因此建议你根据功能将view函数分到不同的模块中。
命令行工具
- 可以自动创始初始工作环境,自动包含必要的目录结构,文件和代码
- 静态文件导出,可以将所有生效的App下的static导出到一个统一的目录
- 提供开发服务器
部署
- 支持GAE部署
- 支持Apache下的mod_wsgi部署
- 支持uwsgi部署
- 支持dotcloud部署
- 支持sae部署
开发
- 提供开发服务器,并当代码修改时自动装载修改的模块
- 提供debug功能,可以查看出错的代码,包括模板中的错误
其它
- 对于静态文件的支持可以处理HTTP_IF_MODIFIED_SINCE和trunk方式的静态文件处理。
项目目标¶
- 开发一个简单易用的框架
- 框架要足够灵活,并易于扩展
- 包含足够的示例代码
- 编写清晰易懂的文档
- 能够在多种环境下使用
链接¶
- Uliweb 项目主页 http://code.google.com/p/uliweb
- Uliweb-doc 文档项目 http://github.com/limodou/uliweb-doc
- Uliweb-doc 在线文档查看链接 http://limodou.github.com/uliweb-doc/
- plugs Uliweb apps收集项目 http://code.google.com/p/plugs
安装说明¶
要求¶
- Python 2.6+ 目前不支持3.X
- setuptools 0.6c11
额外要求¶
- SQLAlchemy 0.6+ (如果使用Uliweb ORM需要安装它)
- pytz (用在uliweb.utils.date和ORM中进行时区的处理,缺省不需要)
最简单的方法是使用easy_install,如:
easy_install Uliweb
另外如果你想跟踪最新的代码,可以使用svn来下载代码,
svn checkout http://uliweb.googlecode.com/svn/trunk/ uliweb
cd uliweb
python setup.py develop
使用develop安装只会在Python/site-packages下建一个链接,并不会真正安装,好处就是更新方便。 不过,当Uliweb的版本升级了,还是要再执行一下安装过程的。
当然你也可以直接通过 install 来安装。
python setup.py install
体系结构和机制¶
组织结构¶
如果你从 svn 中下载 Uliweb 源码,它不仅包括了 Uliweb 的核心组件,同时还包括了 uliwebproject 网站的全部源码和一些示例程序。 Uliweb 采用与 web2py 类似的管理方 式,即核心代码与应用放在一起,到时会减少部署的一些麻烦。但是对于项目的组织是采 用 Django 的管理方式,而不是 web2py 的方式。一个完整的项目将由一个或若干个 App 组织,它们都统一放在 apps 目录下。但 Uliweb 的 app 的组织更为完整,每个 app 有 自已独立的:
- settings.ini 它是配置文件
- templates目录用于存放模板
- static目录用于存放静态文件
- views文件,用于存放view代码
这种组织方式使得Uliweb的App重用更为方便。
在uliweb的下载目录下,基本结构为:
contrib/ #内置的app模块
core/ #核心模块
form/ #form处理模块
i18n/ #国际化处理模块
lib/ #内置的一些库文件,如: werkzeug
locale/ #i18n翻译文件
mail/ #邮件处理
middleware/ #middleware汇总
orm/ #缺省ORM库
template_files/ #用在makeproject, makeapp, support命令上的模板文件
utils/ #输助模块
wsgi/ #wsgi相关的一些模块
manage.py #Uliweb的命令行管理程序
apps的结构为:
apps/
__init__.py
settings.ini
app1/
__init__.py
settings.ini
templates/
static/
app2/
__init__.py
settings.ini
templates/
static/
fcgi_handler.fcgi
wsgi_handler.py
App管理¶
一个项目可以由一个App或多个App组成,而且每个App的结构不一定要求完整,但至少要求 是一个Python的包的结构,即目录下需要一个__init__.py文件。因此一个App可以:
- 只有一个settings.int 这样可以做一些初始化配置的工作,比如:数据库配置,i18n的 配置等
- 只有templates,可以提供一些公共的模板
- 只有static,可以提供一些公共的静态文件
- 其它的内容
Uliweb在启动时对于apps下的App有两种处理策略:
- 认为全部App都是生效的(这种情况比较少见)
- 根据apps/settings.ini中的配置项INSTALLED_APPS来决定哪些App要生效
Uliweb在启动时会根据生效的App来导入它们的settings.ini文件,并将其中配置项进行合
并最终形成一个完整的 settings
变量供App来使用。同时在处理生效的App的同时,
会自动查找所有``views``开头的文件和``views``子目录并进行导入,这块工作主要是为
了收集所有定义在views文件中的URL。
这样当Uliweb启动完毕,所有App下的settings.ini和views文件将被导入。因此,你可以 在settings.ini文件中做一些初始化的工作。
在实际的项目中,apps目录下的settings.ini文件是最后被导入的配置文件,你可以在其 中存放最后生效的配置项,用来替换某些缺省配置。
对于templates和static,Uliweb会首先在当前App下进行搜索,如果没有找到,则去其它 生效的App相应的目录下进行查找。因此,你可以把所有生效的App的templates和static 看成一个整体。所以你完全可以编写只包含templates或static的App,主要是提供一些公 共信息。
URL处理¶
目前Uliweb支持两种URL的定义方式。
一种是将URL定义在每个view模块中,通过expose来定义。
另一种是在settings.ini中的[EXPOSES]中进行定义。这种方式更适合配置化。
URL的格式采用werkzeug的routing模块处理方式。可以定义参数,可以反向生成URL。在 Uliweb中定义了两个方便的函数:
- expose 用来将URL与view方法进行映射
- url_for 用来根据view方法反向生成URL
MVT框架¶
Uliweb 也采用 MVT 的框架。
- Model
- 目前 Model 是基于 SqlAlchemy 封装的 ORM 。
- View
则采函数或类的方式。当 Uliweb 在调用 view 函数时,会自动向函数注入一些对象, 这一点有些象 web2py 。不过 web2py 是基于 exec ,而 Uliweb 是通过向函数注入 变量 (func_globals) 来实现的。这种方式会在某种程序上减少一些导入代码,非常 方便。不过,它只对直接的 view 方法有效,对于 view 函数中又调用的函数无效。 因此你还可以直接通过
from uliweb import request, response
这样的方式来导入一些全局性的对象。
- Template
- 一般你不需要主动来调用, Uliweb 采用自动映射的做法,即当一个 view 函数返回 一个 dict 变量时,会自动查找模板并进行处理。当返回值不是 dict 对象时将不自 动套用模板。如果在 response 中直接给 response.template 指定模板名,可以不 使用缺省的模板。缺省模板文件名是与 view 函数名一样,但扩展名为 .html 。
在使用模板时也有一个环境变量,你可以直接在模板中直接使用预置的对象。同时Uliweb 还提供了对view函数和模板环境的扩展能力。
扩展处理¶
Uliweb提供了多种扩展的能力:
- plugin 扩展。这是一种插件处理机制。 Uliweb 已经预设了一些调用点,这些调用点 会在特殊的地方被执行。你可以针对这些调用点编写相应的处理,并且将其放在 settings.py 中,当 Uliweb 在启动时会自动对其进行采集,当程序运行到调用点位置 时,自动调用对应的插件函数。
- middleware 扩展。它与 Django 的机制完全类似。你可以在配置文件中配置 middleware 类。每个 middleware 可以处理请求和响应对象。
- views 模块的初始化处理。在 views 模块中,如果你写了一个名为 __begin__ 的函数 ,它将在执行要处理的 view 函数之前被处理,它相当于一个入口。因此你可以在这里 面做一些模块级别的处理,比如检查用户的权限。因此建议你根据功能将 view 函数分 到不同的模块中。
全局环境¶
Uliweb提供了必要的运行环境和运行对象,因此我称之为全局环境。
对象¶
有一些全局性的对象可以方便地从 uliweb 中导入,如:
from uliweb import (application, request, response,
settings, Request, Response)
application¶
它是用来记录整个Uliweb项目的运行实例,全局唯一。application是 uliweb.core.SimpleFrame.Dispatcher
的实例,它有一些属性和方法可以让你使用,例如:
- apps
- 将列举出当前application实例所有有效的App名称。它是一个list,比如:
['Hello', 'uliweb.contrib.staticfiles']
- apps_dir
- 当前application的apps的目录。
- template_dirs
- 缺省为当前application所有有效的App的template搜索目录的集合。
- get_file(filename, dir=’static’)
- 从所有App下的相应的目录,缺省是从static目录下查找一个文件。并且会先在当前请求对应 的App下先进行查找,如果没找到,则去其它的App下的相应目录进行查找。
- template(filename, vars=None, env=None)
渲染一个模板,会先从当前请求对应的App下先进行查找模板文件。vars是一个dict对象。env 不提供的话会使用缺省的环境。如果想向模板中注入其它的对象,但不是以vars方式提供,不用 直接修改env,而是通过dispatch功能,绑定:
prepare_view_env
主题就可以了。它会返回渲染后的结果,是字符串。
- render(filename, vars, env=None)
- 它很象template,不过它是直接返回一个Response对象,而不是字符串。
Request¶
而这里的Request类是基于werkzeug的Request来派生的,区别在于:
增加了一些兼容性的内容。原来的werkzeug的Request是没有象GET, POST, params, FILES这 样的属性的,它们分别是:args, form, values, files,为了与其它的Request类兼容,我 添加了GET, POST, params, FILES属性。
Response¶
它也对werkzeug提供的Response进行了派生,区别在于:
添加了一个write方法。而原werkzeug的Response类有一个stream属性,它有write方法。经过 扩展,可以直接使用write方法,会更方便一些。
request¶
request 是上面 Request 类的实例的一个代理对象,并不是一个真正的 Request 对象, response 也是。但是你可以把它当成真正的 Request 和 Response 一样来使用。那么为什 么要这样,为了方便。真正的 Request 和 Response 对象会在收到一个请求时被创建 ,然后存放到 local 中,这样不同的线程将有不同实例。为了方便使用,采用代理方式, 这样用户就不用直接调用 local.reuqest 和 local.response ,而是简单使用 request 和 response 就可以根据不同的线程使用不同的对象了。
Note
request和response是有生存周期的,就是在收到请求时创建,在返回后失效。因此在使用它们 时,要确保你是在它们的生存周期中进行使用的。
在讲View的环境时提到过:写一个view方法时有一些对象可以认为是全局的,其中就包括request和 response,但是这两个对象与其它的不同就是因为它是线程相关并且有生存周期的,其它的则是全局唯 一,并且生存周期是整个运行实例的生存周期。这样,在非view函数中想要使用request和response 对象,一种方式就是在view中传入,但是可能有些麻烦,另一种方式就是通过uliweb来导入,这样就 很方便。
request在行为上和Request一样。
response¶
和request一样是一个代理对象。
settings¶
配置信息对象,这个没什么好说的。
方法¶
如:
from uliweb import (redirect, json, POST, GET,
url_for, expose, get_app_dir, get_apps, function,
functions, decorators, NotFound
)
json¶
def json(data, **json_kwargs):
将一个data处理成json格式,并返回一个Response对象。json_kwargs目前可以允许用户传
入 content_type
值。这样在特殊情况下可以将生成的json数据生成:
content_type = 'text/html; charset=utf-8'
Attention
发现在使用ajaxForm时,因为可能使用了iframe的方式,对于后台返回的json数据,如
果使用 application/json
返回时,在IE浏览器下可能会提示要下载,所以有可能
需要转为 text/html
标式。
POST¶
和expose一样,不过限定访问方法为 POST。
GET¶
和expose一样,不过限定访问方法为 GET。
url_for¶
def url_for(endpoint, **values):
根据endpoint可以反向获得URL,endpoint可以是字符串格式,如: Hello.view.index
, 也可以
是真正的函数对象。
function¶
func = function('function_name')
用户可以在settings.ini中配置供外部使用的函数路径,通过function可以获得这个函数 的对象。例如在settings.ini中如下配置:
[FUNCTIONS]
has_role = 'uliweb.contrib.rbac.has_role'
has_permission = 'uliweb.contrib.rbac.has_permission'
这是uliweb.contrib.rbac中的定义的两个方法,key为方法名,value为方法的路径。 通过:
has_role = function('has_role')
就可以导入真正的函数来使用。
functions¶
这是一个对象,它的作用类似于function,不过它是以属性引用的方式来从settings.ini 中的FUNCTIONS中导入方法,如:
from uliweb import functions
func = functions.hello
相当于:
from uliweb import function
func = function('hello')
decorators¶
它同functions类似的使用方法,但是需要在settings.ini中定义DECORATORS内容,如:
[DECORATORS]
check_role = 'uliweb.contrib.rbac.check_role'
check_permission = 'uliweb.contrib.rbac.check_permission'
使用方法:
from uliweb import decorators
@decorators.check_role('superuser')
@expose('/hello')
def hello():
#...
json_dumps¶
用于将Python的数据结构转为json格式的方法。
json_dumps(obj, unicode=False, encoding=’utf-8’)
unicode为False时,将会把obj中的unicode值转为encoding编码的串。否则转为unicode 描述形式的串。
NotFound¶
404对应的异常类。如果某个链接不存在,将引发这个异常。如果在你的处理中,发现有 不存在的对象,建议使用error来返回。因为NotFound会把当前访问的URL显示出来,可能 不是你想显示的内容。
HTTPException¶
通用的HTTP错误异常类。
Middleware¶
中间件基类,所有 Middleware 类可以从它派生。
UliwebError¶
Uliweb提供了一个通用的异常类 - UliwebError,你可以考虑使用它。
教程:
Simple Todo (Uliweb 版本) 之 基础篇¶
本版本是从 http://simple-is-better.com/news/detail-309 来的,原版本基于web.py开 发的,看到这个应用相对简单,因此我将其改造为uliweb的版本。在uliweb版本中,我会列 举详细的开发过程,同时会指出与web.py的一些差异,有兴趣的可以比较。不过,因为我 没有学过web.py,因此只能从原始代码中进行理解,并推测,如果有说得不对的地方欢迎 与我交流,共同学习。
为什么叫基础篇?因为有基础必然有提高篇,其实目前uliweb已经提供了一些相对实用的 模块或app,可以用来快速开发。在提高篇中我希望使用这些内容重新再写一下Todo这个应 用。因此你会看到有关于plugs和generic view的一些使用。
原始的web.py的版本代码可以从上面的网址找到,那么为了简化,我使用了它的一些文件, 比如样式之类的。好,下面让我们开始体验如何使用uliweb开发这样的todo程序。
Attention
关于如何安装uliweb这里不再描述了,有兴趣地找一找Hello, Uliweb文档。
构建流程¶
创建项目¶
进入一个初始目录(假定为$project),开始创建项目。
进入命令行,执行:
uliweb makeproject simple_todo
上面的命令会在$project下创建一个simple_todo的目录,这个就是我们的项目的目录。
创建todo App¶
uliweb的project是由若干个app组成,所以进入simple_todo目录,然后在命令行执行:
cd simple_todo
uliweb makeapp todo
执行成功后,会在simple_todo/apps下生成一个todo的目录,它就是我们将用来写代码的 主要目录。
功能分析¶
通过阅读源码和查看运行结果的画面,我们可以了解到:这个todo可以支持:
- 显示列表(无翻页)
- 显示的同时可以删除和修改
- 在显示页面上可以直接添加
原教程的数据库配置是使用mysql,这里我使用sqlite,因为比较方便。
基本页面布局¶
为了实现整体的页面布局效果,这里我会创建一个基础的layout.html,它是其它模板的父 模板。(看了原始代码,好象web.py没有模板继承的概念,因此你会看到一个完整的模板 是分散到index.html, header.html和foot.html中去了,这里我们要重构一下。)
在simple_todo/apps/todo/templates下创建layout.html文件。
templates目录是专门用来存放模板文件的地方。
首先是layout.html,以下是修改后的代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{{=settings.SITE.SITE_NAME}}</title>
<link href="{{=url_for_static('styles/reset.css')}}" rel="stylesheet" type="text/css" />
<link href="{{=url_for_static('styles/index/style.css')}}" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="page">
<div class="header box">
<h1><a href="/">{{=settings.SITE.SITE_NAME}}</a></h1>
</div>
<div class="main box">
{{block content}}{{end}}
</div>
<div class="foot">
Copyright© {{=settings.SITE.SITE_NAME}} 作者: {{=settings.SITE.EMAIL}}
</div>
</div>
</body>
</html>
在这里,原来web.py中的变量写法是$config.site_name,变成了{{=settings.SITE.SITE_NAME}}。 在uliweb中,有自已的settings.ini文件,用来存放配置信息,而web.py是使用Python源 文件,uliweb使用类ini文件(它其实是使用自已定义的pyini的格式)。
最重要的一点:
<div class="main box">
{{block content}}{{end}}
</div>
这里定义了一个block,它和django的差不多。不过结束标记不是{{endblock}}而是{{end}}。 原来的web.py版本没有类似的用法。
再有就是这里了:
<link href="{{=url_for_static('styles/reset.css')}}" rel="stylesheet" type="text/css" />
使用{{=url_for_static()}}来生成静态链接。原来的程序是使用:
$config.static/styles/reset.css
这是不一样的地方。
Attention
注意,整个uliweb的教程全部使用utf-8编码(主要指模板及程序文件),以后也建议你使用utf-8编码。
静态文件处理¶
为了方便,我从原来的版本中拷贝了styles目录到simple_todo/apps/todo/static 中去。
settings.ini配置¶
下面配置一下settings.ini文件。
打开simple_todo/apps/settings.ini文件,将其改为:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
'uliweb.contrib.staticfiles',
'todo',
]
[SITE]
SITE_NAME = '任务跟踪'
EMAIL = 'limodou@gmail.com'
其中在INSTALLED_APPS中添加了todo。 ‘uliweb.contrib.staticfiles’是用来专门处理 静态文件的app。
然后是定义了SITE,在下面又定义了SITE_NAME和EMAIL。这里可以使用大写或小写。象django 是必须使用大写的。uliweb的settings.ini格式看上去和ini格式差不多,都是以section 为分隔,然后是key=value的形式。不过,这里的value可以是任意简单的python数据结构, 比如dict, list, tuple, string, unicode等。如果第一行加上#coding=<encoding>,还 可以声明这个ini文件的编码格式。
第一次运行¶
上面的代码目前还无法运行。不过我想看一看大概是什么样了,怎么办。因为目前,我们只 完成了:
- layout.html模板
- settings.ini的基本定义(数据库还没定义)
所以还差得远了。为了运行,我们首先要修改一下simple_todo/apps/todo/views.py,改 为:
#coding=utf-8
from uliweb import expose
@expose('/')
def index():
return {}
上面的代码,将定义一个views函数。使用@expose来定义它对应的url。这是与web.py和django 不同的地方。在uliweb中,url一般是定义在views.py文件中的,通过decorator与view函数 进行绑定。
上面index()将返回一个{}。那么表示它将使用缺省的模板,模板名就和view函数名一样, 在这里是index。所以我们还需要在todo/templates中定义一个index.html。
本来,index.html中需要定义如果展示todo的内容,但是因为目前数据库等内容还没有创建, 所以我们只想显示空的内容。
在todo/templates下创建index.html,内容为:
{{extend "layout.html"}}
的确,目前只有这一行代码。它表示从layout.html这个父模板中进行继承。
好,目前差不多了,让我们回到命令行,在simple_todo目录下运行:
uliweb runserver
如果没有错误,则会看到:
* Loading DebuggedApplication...
* Running on http://localhost:8000/
* Restarting with reloader...
* Loading DebuggedApplication...
说明,开发服务器已经准备完毕了,可以通过访问 http://localhost:8000 来看效果了。 可以看到如下的效果:
添加数据库配置¶
基本架子已经搭好。下面是进行数据库配置。
打开apps/settings.ini,修改为:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
'uliweb.contrib.staticfiles',
'uliweb.contrib.orm',
'todo',
]
[SITE]
SITE_NAME = '任务跟踪'
EMAIL = 'limodou@gmail.com'
[ORM]
CONNECTION = 'sqlite:///database.db'
AUTO_CREATE = False
这里的重点是添加’uliweb.contrib.orm’这个app,然后是将它要使用的配置信息放在[ORM] 中,这里主要是配置了sqlite数据库,并且使用了相对路径,因此,以后再运行时,database.db 将会在simple_todo这个目录下。
注意AUOT_CREATE=False,它的作用就是当使用某个Model时,不自动创建Model。缺省情况 下是自动创建,这样只要Model不存在,Uliweb就会自动创建。但是发现对于sqlite,如果 在事务中,执行了非select, update, delete等语句,会引发事务自动提交,造成不一致。 因此这里我就把它关掉了。
Attention
Uliweb有自已的ORM,你可以选择使用,也可以选择不使用。这里是使用了自带的ORM。 同时Uliweb的ORM是基于sqlalchemy开发的,因此上面的数据库连接串是和sqlalchemy 一致的。
原来版本中使用的是mysql,如果你想试一下,可以将上面的CONNECTION的内容改为:
CONNECTION = 'mysql://todo:123456@localhost/todo?charset=utf8'
最后的charset=utf8可以根据需要来选择,这里会强制设置client使用utf8编码。
创建Todo的Model¶
有了库,下面就是创建表结构。在todo下创建models.py文件,写入以下内容:
#coding=utf-8
from uliweb.orm import *
class Todo(Model):
title = Field(str, verbose_name="标题", max_length=255, required=True)
post_date = Field(datetime.datetime, verbose_name='提交时间', auto_now_add=True)
finished = Field(bool, verbose_name='是否完成')
这里我们定义了3个字段。因为我没有发现web.py版本中有创建表的内容,所以我根据代码 理解大概有这么几个字段。不过原版本好象没有实现完成状态的设置,所以我这里预留了。
让我看一下代码。在Uliweb中,可以通过从Model派生出新的子类。它和django的Model类似。 不过这里在定义字段时有两种方式,一种是直接使用真正的字段类,如:StringProperty, DatetimeProperty,不过这种不是很好记忆,而且输入字符比较多,因此还提供简化的定义 方式。通过Field()函数来定义,它的第一个参数是字段的类型,都是基本的Python type对象, 如:str, bool, int, flat, datetime, date等。但是有一些数据库结构中有,但是不存在 对应的Python类型,如:TEXT,等,或需要单独导入的某些特殊的类,如:decimal.Decimal 等,在uliweb,分别定义了大写的类型,如:TEXT, BLOB, DECIMAL可以直接使用。
其它的参数相对直观。对于post_date字段,使用了auto_now_add=True,它的作用就是 当创建新记录时,会自动使用系统当前时间填充,这样你可以不用给它赋值。这一点和 django的一样。
Attention
在定义Model时,我们一般使用首字母大写的单词作为Model的名字。但是uliweb会自 动将其转为小写。所以Todo类对应的表名,其实是todo。
定义完Todo后,我们还有一项配置工作,那就是把Model配置到settings.ini中去。有两种 做法,一种是放到apps/settings.ini中去,但是这样不方便移植,所以还可以放到todo/settings.ini 中去。不过现在没有这个文件,因此让我们创建一个,然后输入以下内容:
[MODELS]
todo = 'todo.models.Todo'
key是todo,即真正的表名,值是todo对应的类的路径,格式为:app_name.models.Model_name
下面,让我们在命令行下创建这个表。其实,如果不设置前面的AUTO_CREATE = False,则 随着运行,Todo表会自动创建,但是现在让我们手工创建,顺便看一看会不会报错。
在命令行下运行:
uliweb -v syncdb
可以看到:
Creating todo...
然后还可以输出相应的建表的sql语句:
> uliweb sql
CREATE TABLE todo (
post_date DATETIME,
finished BOOLEAN,
id INTEGER,
title VARCHAR(255),
PRIMARY KEY (id),
CHECK (finished IN (0, 1))
)
如果我们到simple_todo目录下看,可以发现database.db已经创建好了。
等等,上面怎么好象多了一个id的字段。没错,和django一样,uliweb orm会自动为每个 表创建一个id的字段。
显示Todo¶
下面开始写展示Todo列表的代码了,让我们先从模板开始。我们需要再次编辑index.html了, 让我们写入下面的代码:
{{extend "layout.html"}}
{{block content}}
<div class="box">
<div class="box todos">
<h2 class="box">待办事项</h2>
<ul>
{{for todo in todos:}}
<li>
{{=todo.title}}
<span class="action">
<a href="/todo/edit/{{=todo.id}}">修改</a>,
<a href="/todo/delete/{{=todo.id}}"
onclick="return confirm('删除以后不能恢复的,确定?')">删除</a>
</span>
</li>
{{pass}}
</ul>
</div>
<div class="box post">
<h2>新增</h2>
<form action="/todo/new" method="post" id="post_new">
<p><input type="text" name="title" class="long_txt" /></p>
<p><input type="submit" class="submit" value="添加" /></p>
</form>
</div>
</div>
{{end}}
这段代码是我从web.py版本中拷贝并修改的,它主要包含两部分:
- 显示Todo列表的循环
- 显示添加新的Todo的内容
第一部分比较简单,我们希望向模板中传入一个todos的变量,它其实是所有todo的一个列表。 然后,在模板中进行循环。Uliweb的模板可以直接写Python代码,所以for后面的’:’不要忘了。 同时for结束(包括其它的块语句结束,如:if, def, while等)都要在后面加上{{pass}}, 用来标识块缩近结束。所以在uliweb中你不用考虑缩近,但是要在适应的位置加上{{pass}}。
在循环中,我们会显示Todo的标题,同时展示两个链接。这里我使用了和web.py版本不同的 格式,原来的是/todo/id/edit,看上去更RESTFul一些,我使用的是/todo/edit/id,为什么? 其实也可以和原版本保持一致,不过,我想在views.py中展示如何使用class view的写法, 如何省事,所以就使用了这种格式。写成原来的格式也是可以的。
模板准备好了,下面写views.py了。
最开始我们运行时,我们看到在views.py中定义了一个函数,这是和django相一致的。现在 django 1.3已经支持class方式的view了,Uliweb中也支持类似的方式,不过和django的差 异很大。同时和web.py的方式也不同。我看web.py的方式和django的更接近一些。
通过看原版本代码,web.py的类只用来处理一个URL,同时可以区分不同的方法,如:GET, POST 等。而uliweb对GET, POST的区分是通过URL的定义来实现的,class本身可以同时支持多个URL。 因此,在原版本中,你会在todo.py中看到针对不同的请求,分别定义了:New, Edit, Delete, Index。 而我将只用一个Todo类来定义,增加不同的方法。
views.py 的代码如下:
#coding=utf-8
from uliweb import expose
@expose('/todo')
class Todo(object):
def __init__(self):
from uliweb.orm import get_model
self.model = get_model('todo')
@expose('/')
def index(self):
return {'todos':self.model.all()}
我把原来的代码删除了。简单解释一下:
在Uliweb中class view的class可以是new style class,也可以是classic style class, 不过建议使用new style class。
类上也可以加@expose(),这样,类中所有的方法都会带上这个前缀,除非你覆盖它,正 如下面的index一样。
你可以在__init__中写一些初始化的代码。上面就是定义了要使用的model。这里get_model() 的使用是uliweb的一个创新(我认为是这样的)。虽然在前面,我们定义Model的时候好 象麻烦了点,因此还要修改settings.ini。但是这里就方便了。我们甚至不需要知道 todo表在哪里,就可以直接导入。
对于index,这里又定义了一个@expose(‘/’),那么它将会覆盖缺省的URL定义。
index将返回一个字典。获得一个Model的所以记录就是Model.all()。这里不象django 一样,还要加上objects,不需要。
原教程中在列出所有todo时还对id进行了升序排列,但是缺省都是按主键排列,而id正好是 主键,所以这里我就省了。当然,如果你想加的话可以这样:
self.model.all().order_by(self.model.c.id)
这里的语法完全和sqlalchemy是一致的。在Model中有一个和sqlalchemy Table一样的 c属性,可以用来引用字段。这里就不多说了。
让我们运行一下,刷新一下界面。
不好,报错了,说是使用了“Default Template”,这是怎么回事?
因为我们使用了class view的方式,所以对于模板目录有一个小小的变化,那就是要在 templates中定义一个和Todo一样的目录,然后将index.html放到这个下面。这样,所有 在class view中定义的方法对应的模板都放到相应的类目录中。
改完以后,再运行,结果是这个样子。
实现新增Todo¶
只要能添加就好办了。下面写添加代码:
def new(self):
title = request.POST.get('title')
if not title:
error('标题是必须的')
todo = self.model(title=title)
todo.save()
return redirect(url_for(Todo.index))
注意这是Todo的一个新方法,要注意缩近。解释一下:
new上没有定义@expose(),所以它的url将会是/todo/new
有人要问,如果有些方法不想有真正的URL怎么办,那么所有以_开头的方法都不会对应 一个URL,也就不会被人访问到。
在uliweb,有些方法和变量是全局的,比如上面的:request, error, redirect,都是 可以直接使用的。如果你想显示地使用它们,可以通过:
from uliweb import request, error, redirect
error目前会引发一个异常,所以并不需要return
url_for可以反向获取一个URL,这里传入的是一个函数对应,所以url_for(Todo.index) 其实就是’/’。这种做法主要是因为:代码结构可能不容易变化,而URL却容易变化,通过 反向获取,会减少URL变化带来的修改。当然,你可以不用,直接写’/’。
在uliweb中,instance=Model(**kwargs)可以用来创建一条记录,当然要注意使用instnace.save() 来保存。上面我们没有传入post_date,但是由于在Model定义时,我们加入了auto_now_add, 所以在创建新记录时,它会自动使用服务器的时间。
Uliweb的Request, Response目前是使用werkzeug(和Flask基础是一样的)库。
上面的代码将判断是否有title,如果没有则报错。如果有,则保存。让我们运行一下。
让我们输入:这是一个测试
如果我们什么都不输会怎么样?
怎么回事,又报错!晕啊,程序真是不好写,Uliweb不好玩啊。先看下界面吧:
这是一个调试界面,在Uliweb中使用了werkzeug的调试器,可以在Debug状态下,当出错时 显示出错界面,非常不错。上面的错误就是找不到error.html模板。为什么?因为我们没 有定义这样的一个模板。好吧,让我们在todo/templates下创建一个,因为它不是与class view 对应,所以不需要放在Todo目录下。
创建的error.html代码如下:
{{extend "layout.html"}}
{{block content}}
<div class="content">
<h1 style="font-weight:400;">
出错了!{{=message}}
</h1>
<p>
<a href="javascript: history.back();">返回</a> |
<a href="/">首页</a>
</p>
</div>
{{end}}
这里放置一个message的变量,它是由error传入的。这个模板与web.py版本不完全一样,原 版本还有自动跳转,我这里没有。
这里我没有特别区分URL是GET还是POST,比如上面的new,如果希望使用POST接收,可以 在new上写:
@expose(methods=['POST'])
修改Todo¶
下面开始处理修改Todo了。首先创建模板吧,在Todo目录下创建edit.html,内容为:
{{extend "layout.html"}}
{{block content}}
<div class="box post">
<h2>修改</h2>
<form action="" method="post">
<p><input type="text" name="title" class="long_txt" value="{{=todo.title}}" /></p>
<input type="submit" class="submit" value="提交" />
</p>
</form>
</div>
{{end}}
这里没什么可讲的。
然后是写修改的view代码:
def edit(self, id):
todo = self.model.get(int(id))
if not todo:
error('没找到这条记录')
if request.method == 'GET':
return {'todo':todo}
else:
title = request.POST.get('title')
if not title:
error('标题是必须的')
todo.title = title
todo.save()
return redirect(url_for(Todo.index))
解释一下:
- 现在edit有一个参数,那么自动生成的URL是什么样的?答案是/todo/edit/<id>,这也 就是为什么我前面要修改/todo/id/edit的原因,就是为了和class view生成的URL相一致。
- edit这个函数在编辑时会执行两次,第一次是判断request.method==’GET’时,用于显示。 第二次是在修改后提交时,用于保存。在web.py的版本中,我们看到它是在Edit这个类中 通过定义了GET和POST方法来进行区分,而在Uliweb中则通过判断语句来进行区分,功能 一样,思路有所差别。不过,原来的版本中有关于记录是否存在的判断在GET和POST中 有相同的代码,而在Uliweb的版本中进行了合并。
- 使用self.model.get(id)就可以获得一个对象。如果不存在,会返回None,而不是异常。
- todo.title = title 然后 todo.save() 这是典型的ORM操作。
不过写到这里,和web.py版本有所不同。主要是在成功后,web.py版本会利用error页面, 因为它有自动跳转的功能,来显示成功后的信息。而我删除了,所以在我的版本中不会显示 修改成功后的信息。不过,如果要做有很多种方式 ,比如通过session机制。或使用uliweb 中提供的flashmessage app等。这里就不再演示了。
删除Todo¶
删除比较简单,不需要处理模板,修改views.py代码:
def delete(self, id):
todo = self.model.get(int(id))
if not todo:
error('没找到这条记录')
todo.delete()
return redirect(url_for(Todo.index))
先检查是否id存在,然后删除,接着重定向。
简单重构¶
上面的修改和删除都会判断id是否存在,那么可以提成一个函数,函数以_开头就可以了, 所以views.py的最后版本为:
#coding=utf-8
from uliweb import expose
@expose('/todo')
class Todo(object):
def __init__(self):
from uliweb.orm import get_model
self.model = get_model('todo')
@expose('/')
def index(self):
return {'todos':self.model.all()}
def new(self):
title = request.POST.get('title')
if not title:
error('标题是必须的')
todo = self.model(title=title)
todo.save()
return redirect(url_for(Todo.index))
def _get_todo(self, id):
todo = self.model.get(int(id))
if not todo:
error('没找到这条记录')
return todo
def edit(self, id):
todo = self._get_todo(id)
if request.method == 'GET':
return {'todo':todo}
else:
title = request.POST.get('title')
if not title:
error('标题是必须的')
todo.title = title
todo.save()
return redirect(url_for(Todo.index))
def delete(self, id):
todo = self._get_todo(id)
todo.delete()
return redirect(url_for(Todo.index))
再说一说URL的定义¶
因为我使用了class view方式,所以你基本上看不到复杂的URL的定义,那么在web.py的版 本中是如何的呢:
pre_fix = 'controllers.'
urls = (
'/', pre_fix + 'todo.Index',
'/todo/new', pre_fix + 'todo.New',
'/todo/(\d+)', pre_fix + 'todo.View',
'/todo/(\d+)/edit', pre_fix + 'todo.Edit',
'/todo/(\d+)/delete', pre_fix + 'todo.Delete',
)
这里可以看到它使用的是正则式,标准的正则式,和django一样。那么如果在Uliweb中定义 象上面的URL会是什么样子:
/todo/<int:id>
/todo/<int:id>/edit
/todo/<int:id>/delete
就是这个样子。Uliweb中的URL定义也是来自于werkzeug,它是一种简化的定义方式,我认 为比原始的正则式要简单很多。
后记¶
非常感谢Ken提供了Todo的web.py的教程,才使得Uliweb版本的教程得以出现。我一直很想 把有关class view的内容讲得更清楚,这次多少涉及了大部分,还有一些没有涉及。另外就 是如何利用我已经实现的一些app来简化开发。从上面的处理可以看到,最基本的就是:CRUD 的操作了。django是通过generic view和admin来实现。Uliweb目前也有一个类似的generic view的东西,我想有机会在下一个提高教程中向大家展示这个东西。当然,它会基于更多的 依赖,所以未必会适合你,但是却是一个我认为不错的扩展的思路。
写得比较仓促,欢迎与我讨论。程序代码可以从 https://github.com/limodou/uliweb-doc 中的simple_todo中找到,包括我使用sphinx写的教程。
Simple Todo (Uliweb 版本) 之 高级篇¶
本版本是从 http://simple-is-better.com/news/detail-309 来的,并且已经使用Uliweb 实现了一个 基础版本 ,不过 这个基础版本讲述的是最基本的Uliweb的用法,因此叫基础篇。 在这个高级篇中,我希望向大家介绍更加高级一些的内容。其中有些功能要依赖于plugs和 generic的一些功能。
plugs是我在开发时把认为可以复用的一些app发布出来,供大家使用。它收集了象基本的 页面布局,通用的view函数,jquery相关的一些界面代码和控件,以及其它的一些配套 功能。但是在这个教程中只会使用部分功能。plugs本身是需要安装,你可以直接下载源码 进行安装,也可以通过 easy_install plugs 或 pip install plugs 来安装。不过因 为目前plugs还在不断优化中,因此建议直接通过下载源码来安装。现在plugs可以在 http://code.google.com/p/plugs 和 https://github.com/limodou/plugs 都可以找到。
generic在plugs中存在一个app,它提供了封装好的view函数,可以直接使用。类似于django 的generic_view的功能。它提供了class-based的view和function-based的view方法,你可 以根据需要来使用。
同时在uliweb中还提供了generic.py模块,它提供了更底层的view的调用,分别对应于: list, add, edit, view, delete。而plugs中的generic又是使用了generic.py模块的功能 来实现。generic.py的功能我认为非常强大,我会用专门的文档来进行介绍。
构建流程¶
构建初始环境¶
在基础篇中讲过的内容,这里不再缀述。假定目前初始为$project,操作如下:
uliweb makeproject plugs_todo
cd plugs_todo
uliweb makeapp todo
settings.ini配置¶
修改apps/settings.ini为以下内容:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
'plugs.layout',
'uliweb.contrib.orm',
'todo',
]
[ORM]
CONNECTION = 'sqlite:///database.db'
AUTO_CREATE = False
[I18N]
LOCALE_DIRS = ['${plugs}']
SUPPORT_LANGUAGES = ['en', 'zh_CN']
ORM段中我使用了sqlite数据库,同时关闭自动创建表的选项。在 INSTALLED_APPS
中加入
了’plugs.layout’和’todo’这两个app。由于使用了plugs,它是支持i18n的处理,因此上面
I18N一段就是相关的配置。在这个小网站,我们可以使用英文和中文。
基本布局¶
前面我们看到了,已经加入了 plugs.layout
这个app,它提供了基本的布局,因此我
们可以直接使用它。在layout中的settings.ini中有几个参数可以设置,缺省值为:
[LAYOUT]
COPYRIGHT = _('All rights reversed!')
TITLE = _('Uliweb Plugs Demo')
MENUS = [
('home', _('Home'), '/'),
('admin', _('Admin'), '/user/view'),
('about', _('About'), '/about'),
]
让我们修改一下,添加到apps/settings.ini中去:
[LAYOUT]
COPYRIGHT = 'Copyright© 任务跟踪 作者: limodou@gmail.com'
TITLE = '任务跟踪'
MENUS <= [
('home', '首页', '/'),
]
这里有一个特别的地方,那就是MENUS <= 它的作用是替换原来的值。因为pyini在处理时,
如果遇到相同名字的两个变量,如果变量是不可变对象,则直接替换,如:str, int等。但
如果值是可变对象,如:list, dict,则会进行合并。因为上面的MENUS是list,所以缺省
情况下会进行合并,而我们的目的是想替换,所以使用 <=
就可以替换了。
为了运行,我们首先要修改一下plugs_todo/apps/todo/views.py,改 为:
#coding=utf-8
from uliweb import expose
@expose('/')
def index():
return {}
然后我们在plugs的layout.html的基础上,来创建一个通用的基础模板,如命名为basel.html, 内容如下:
{{extend "layout.html"}}
{{block userinfo}}{{end}}
因为layout.html中还有用户的信息,但是我们这里并不使用,因此,我们将block userinfo 置为空。
然后在todo/templates下创建index.html,内容为:
{{extend "base.html"}}
好,目前差不多了,让我们回到命令行,在plugs_todo目录下运行:
uliweb runserver
如果没有错误,可以通过访问 http://localhost:8000 看到如下界面:
创建Todo的Model¶
下面就是创建Todo表的结构。在todo下创建models.py文件,写入以下内容:
#coding=utf-8
from uliweb.orm import *
class Todo(Model):
title = Field(str, verbose_name="标题", max_length=255, required=True)
post_date = Field(datetime.datetime, verbose_name='提交时间', auto_now_add=True)
finished = Field(bool, verbose_name='是否完成')
在todo下创建settings.ini,然后输入以下内容:
[MODELS]
todo = 'todo.models.Todo'
这样我们就将todo表做成配置化的了。关于配置化,详情可以查看 Uliweb ORM 的文档。
在命令行下运行:
uliweb syncdb
来创建表。
处理Todo¶
下面开始写展示Todo列表的代码了,让我们看一下如何使用plugs.generic中的View Class。 编辑 todo/views.py 如下:
#coding=utf-8
from uliweb import expose
from plugs.generic.views import View
@expose('/')
class Todo(View):
model = 'todo'
layout = 'base.html'
key_field = 'title'
# add_button_text = _('New')
# pagination = True
# rows = 10
@expose('/')
def list(self):
return View.list(self)
可以看到我们从plugs.generic.views中导入了View类,然后从这个类派生了Todo的子类。 View类中已经预定义了象list, view, edit, delete, add等方法。基本的功能,包括展示 已经全部由View来实现,你只要进行必要的配置就可以实现一个非常快速的录入。当然, 在实际的项目中我们可能不会这么简单,但是这至少是一个可以表现复用性的例子。
在Todo中还定义了一些类属性,用于具体的配置。如model对应要处理的表。layout表示整 体要使用的布局文件,这里是base.html。key_field是表示在显示列表时,哪个字段将展 示为相应的查看链接。其它的给注释了,它们显示的都是相应的缺省值。可以看到它还包含 了相应的分页的处理。
改完以后,再运行,结果是这个样子。
这里的代码已经完成了整个的:列表显示,增加,删除,修改,查看的功能。但是它是以 一种预定义的方式来展示的,可能不能满足你的要求,但是作为快速开发会非常方便。
你仍然可以使用更底层一些的generic.py来实现更加个性化的代码。
后记¶
其实在plugs的相应的app中封装了好多的东西。比如flash message,表格的分页处理, 由Model转为Form的机制,下载的处理等。还包括一些ui的处理,如对jquery easyui的封 装等。它们都是构成plugs的基础,我会不断完善它们。
Hello, Uliweb¶
本教程将带你领略 Uliweb 的风采,这是一个非常简单的例子,你可以按照我的步骤来操作。我们 将生成一个空的页面,它将显示”Hello, Uliweb”信息。
创建新的项目¶
在安装完毕后,Uliweb 提供一个命令行工具 uliweb, 它可以执行一些命令,它会安装在 Python/Scripts 目录下,因此要想运行它,要保证 Python/Scripts 在PATH环境变量上,这样我们就可以在命令行 下使用它了。
进入你的工作目录,然后执行:
uliweb makeproject project
执行成功后,在 project 目录下会是这样的:
|-- app.yaml
|-- apps/
| `-- settings.ini
|-- gae_handler.py
|-- runcgi.py
`-- wsgi_handler.wsgi
创建Hello应用¶
然后让我们创建一个Hello的应用:
cd project
uliweb makeapp Hello
在执行成功后,你会在apps/Hello下看到:
|-- __init__.py
|-- conf.py
|-- info.ini
|-- static/
| `-- readme.txt
|-- templates/
| `-- readme.txt
`-- views.py
输出”Hello, Uliweb”¶
打开 Hello/views.py,你会看到:
#coding=utf-8
from uliweb import expose
@expose('/')
def index():
return '<h1>Hello, Uliweb</h1>'
以上几行代码是在执行 makeapp 之后自动创建的。甚至我们都不用写一行代码,已经有一个 Hello, Uliweb 的View函数了。
@expose(‘/’) 是用来处理 URL Mapping的,它表示将/映射到它下面的view方法上。这样,当用户 输入 http://localhost:8000/ 时将访问 index() 方法。如果一个函数前没有使用expose修饰, 它将不会与任何URL对应,因此可以认为是一个局部函数。
这里index()没有任何参数。如果你在expose中定义了参数,它将与之对应。但因为这个例子没有定 义参数,因此index不需要定义参数。
然后我们直接返回了一行HTML代码,它将直接输出到浏览器中。
启动¶
好了,让我们启动看一下结果吧。
在命令行下执行:
uliweb runserver
这样就启动了一个开发服务器。然后可以打开浏览器输入: http://localhost:8000 看到结果。
是不是很简单,但是这样不够,让我们变化一下,这次让我们加入模板。
加入模板¶
如果你的 view 方法返回一个dict对象,则 Uliweb 会自动为你应用一个模板,模板名字与你的view 方法一样,只不过后面有一个 .html。如 index() 对应的模板就是 index.html。那么这个模板文件 放在哪里呢?在前面你可以看到,当你创建完一个 app 之后,会自动创建一个 templates 目录,因 此你的模板就放在这个 templates 目录下。好,为了不影响index()方法,让我们创建一个新的方法
@expose('/template')
def template():
return {}
然后在apps/Hello/templates下创建 template.html, 内容为:
<h1>Hello, Uliweb</h1>
在浏览器输入 http://localhost:8000/template 你将看到相同的结果。
使用模板变量¶
上面的例子是将信息全部放在了模板中,但是这样通用性不好,现在再让我们修改一下,使用模板变量。 让我们再创建一个新的view方法,写入下面的代码:
@expose('/template1')
def template1():
return {'content':'Uliweb'}
然后在apps/Hello/templates下创建 template1.html,内容为:
<h1>Hello, {{=content}}</h1>
这次我在template1()中返回了一个字典,则变量content将用来表示内容。也许你对使用 {} 这样 的形式感觉不够方便,还有以下的变形的方式,如:
return dict(content='Uliweb')
或:
content = 'Uliweb'
return locals()
前一种方法利用dict函数来构造一个dict对象。而后一种方法则直接使用了locals()内置函数来返 回一个dict对象,这样你只要定义了相应的变量就可以了。这样locals()返回的变量有可能比模板 所需要的变量要多,但是不会影响你的使用,只要在模板中认为不存在就可以了。
Note
使用 Uliweb 的开发服务器具备自动重启的功能,因此一般进行程序的修改不需要重启服务器, 只要刷新浏览器就行。但有时程序出错或一些模板具备缓冲能力还是需要刷新。只要在命令行下 输入 Ctrl+C 就可以结束开发服务器,然后重启就行。
迷你留言板¶
也许你已经学过了 Hello, Uliweb 这篇教程,对Uliweb已经有了一个感性的 认识,那么好,现在让我们进入数据库的世界,看一看如何使用简单的数据库。
准备¶
在 uliweb-tests 项目中已经有完整的GuestBook的源代码,你可以从它里面检出:
svn checkout http://uliweb-tests.googlecode.com/svn/trunk/guestbook guestbook
cd guestbook
uliweb runserver
然后在浏览器输入 http://localhost:8000/ 这样就可以看到了。目前缺省是使用 sqlite3。如果你安装了python 2.5它已经是内置的。否则要安装相应的数据库和Python的绑定模 块。目前Uliweb使用 SqlAlchemy 作为数据库底层驱动, 它支持多种数据库,如:mysql, sqlite, postgresql, 等。
好了,源码准备好了,下一步,准备开发环境。
创建APP¶
进入前面创建的目录,然后使用 makeapp 建一个新的App。执行:
cd guestbook
uliweb makeapp GuestBook
这样就自动会在项目的apps目录下创建一个 GuestBook
的App。
配置数据库¶
Uliweb中的数据库不是缺省生效的,因此你需要配置一下才可以使用。Uliweb虽然提供了自已的
ORM,但是你可以不使用它。Uliweb提供了插件机制,可以让你容易地在适当的时候执行初始化的工作。
打开 apps/GuestBook/settings.ini
文件,修改 INSTALLED_APPS
的内容为:
INSTALLED_APPS = [
'GuestBook',
'uliweb.contrib.orm',
]
然后添加下面的内容:
[ORM]
CONNECTION = 'sqlite:///guestbook.db'
所以 settings.ini
将看上去象:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
'GuestBook',
'uliweb.contrib.orm',
]
[ORM]
CONNECTION = 'sqlite:///guestbook.db'
ORM.CONNECTION 是ORM的连联字符串,它和SQLAlchemy包使用的一样。通常的格式看上去象:
provider://username:password@localhost:port/dbname?argu1=value1&argu2=value2
对于Sqlite,连接信息有些不同:
sqlite_db = create_engine('sqlite:////absolute/path/to/database.txt')
sqlite_db = create_engine('sqlite:///d:/absolute/path/to/database.txt')
sqlite_db = create_engine('sqlite:///relative/path/to/database.txt')
sqlite_db = create_engine('sqlite://') # in-memory database
sqlite_db = create_engine('sqlite://:memory:') # the same
这里我们使用相对路径格式,所以 guestbook.db
将会在guestbook目录下被创建。
模板环境的扩展¶
向 GuestBook/__init__.py
中添加:
from uliweb.core.dispatch import bind
@bind('prepare_view_env')
def prepare_view_env(sender, env, request):
from uliweb.utils.textconvert import text2html
env['text2html'] = text2html
这也是一个dispatch的使用示例,它将向模板的环境中注入一个新的函数 text2html
,
这样你就可以在模板中直接使用text2html这个函数了。
准备Model¶
在GuestBook目录下创建一个名为models.py的文件,内容为:
from uliweb.orm import *
class Note(Model):
username = Field(CHAR)
message = Field(TEXT)
homepage = Field(str, max_length=128)
email = Field(str, max_length=128)
datetime = Field(datetime.datetime, auto_now_add=True)
很简单。
首先要从 uliweb.orm 中导入一些东西,这里是全部导入。
Uliorm在定义Model时支持两种定义方式:
- 使用内部的Python类型,如:int, float, unicode, datetime.datetime, datetime.date, datetime.time, decimal.Decimal, str, bool。另外还扩展了一些类型,如:BLOB, CHAR, TEXT, DECIMAL。 所以你在定义时只要使用Python的类型就好了。
- 然后就是象GAE一样的使用各种Property类,如:StringProperty, UnicodeProperty, IntegerProperty, BlobProperty, BooleanProperty, DateProperty, DateTimeProperty, TimeProperty, DecimalProperty, FloatProperty, TextProperty。
一个Model需要从 Model
类派生。然后每个字段就是定义为类属性。Field()是一个函数,它将
会根据第一个参数来查找对应的属性类,因此:
class Note(Model):
username = StringProperty()
message = TextProperty()
homepage = StringProperty()
email = StringProperty()
datetime = DateTimeProperty()
每个字段还可以有一些属性,如常用的:
- default 缺省值
- max_length 最大值
- verbose_name 提示信息
象CharProperty和StringProperty,需要有一个max_length属性,如果没有给出,缺省是30。
其它详细的说明可以在数据文档中查看。
Note
在定义Model时,Uliorm会自动为你添加 id
字段的定义,它将是一个主键,这一
点与Django一样。
静态文件处理¶
我们将在后面显示静态文件,现在只需要把 uliweb.contrib.staticfiles
添加到 INSTALLED_APPS
中就可以了。使用这个App,所有有效的app的static目录将被处理为静态目录,并且URL链接将添加
/static/
。现在 settings.ini
看上去象:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
'GuestBook',
'uliweb.contrib.orm',
'uliweb.contrib.staticfiles',
]
[ORM]
CONNECTION = 'sqlite:///guestbook.db'
显示留言¶
增加guestbook()的View方法¶
打开GuestBook下的views.py文件,加入显示留言的处理代码:
from uliweb import expose
from models import Note
@expose('/')
def index():
notes = notes = Note.all().order_by(Note.c.datetime.desc())
return {'notes':notes}
在开始的地方,我们导入了Node类。后面我们会用到。
然后使用expose()来定义URL为 /
。
然后是index()函数的定义。我们通过调用Node类的方法all()获得所有
记录。为了按时间倒序显示,使用order_by()方法,传入要按顺的字段。其中
Note.c.datetime.desc()
是Sqlalchemy的用法,表示倒序。
以下是一些简单的用法:
notes = Note.all() #全部记录,不带条件
note = Note.get(3) #获取id值为3的记录
note = Note.get(Note.c.username=='limodou') #获取username为limodou的记录
然后我们返回一个字典,这样会自动使用Uliweb的模板套用机制,即自动调用与view方法 同名的模板文件。
Note
在Uliweb中每个访问的URL与View之间要通过定义来实现,如使用expose。它需要一个URL的 参数,然后在运行时,会把这个URL与所修饰的View方法进行对应,View方法将转化为:
appname.viewmodule.functioname
的形式。它将是一个字符串。然后同时Uliweb还提供了一个反向函数url_for,它将用来根据 View方法的字符串形式和对应的参数来反向生成URL,可以用来生成链接,在后面的模板中我 们将看到。
定义index.html模板¶
在GuestBook/templates目录下创建与View方法同名的模板,后缀为.html。在index.html中 添加如下内容:
{{extend "base.html"}}
{{block content}}
<h2><a href="{{=url_for('GuestBook.views.new_comment')}}">New Comment</a></h2>
{{for n in notes:}}
<div class="info">
<h3><a href="{{= url_for('GuestBook.views.del_comment', id=n.id) }}">
<img src="{{= url_for_static('delete.gif') }}"/>
</a> {{=n.username}} at {{=n.datetime.strftime('%Y/%m/%d %H:%M:%S')}} say:</h3>
<p>{{<<text2html(n.message)}}</p>
</div>
{{pass}}
{{end}}
第一行将从base.html模板进行继承。这里不想多说,只是要注意在base.html中有一个{{block content}}{{end}} 的定义,它表示子模板可以继承的块。你可以从Uliweb的源码中将base.html拷贝到你的目录下。
h2 标签将显示一个链接,它将用来调用添加留言的view函数。注意模板没有将显示与添加的 Form代码写在一起,因为那样代码比较多,同且如果用户输入出错,将再次显示所有的留言(因为这里 没有考虑分页),这样处理比较慢,所以分成不同的处理了。
{{for}}
是一个循环。记住Uliweb使用的是web2py的模板,不过进行了改造。所有在{{}}中的代码
可以是任意的Python代码,所以要注意符合Python的语法。因此后面的’:’是不能省的。Uliweb的模
板允许你将代码都写在{{}}中,但对于HTML代码因为不是Python代码,要使用 out.write(htmlcode)
这种代码来输出。也可以将Python代码写在{{}}中,而HTML代码放在括号外面,就象上面所做的。
在循环中对notes变量进行处理,然后显示一个删除的图形链接,用户信息和用户留言。
看到 {{<<text2html(n.message)}}
了吗?它使用了我们在GuestBook/__init__.py中定义的text2html函
数对文本进行格式化处理。
{{pass}}
是必须的。在Uliweb模板中,不需要考虑缩近,但是需要在块语句结束时添加pass,表示缩
近结果。这样相当于把Python对缩近的严格要求进行了转换,非常方便。
好,在经过上面的工作后,显示留言的工作就完成了。但是目前还不能添加留言,下一步就让我们看如 何添加留言。
Note
因为在base.html中和guestbook.html用到了一些css和图形文件,因此你可以从Uliweb的 GuestBook/static目录下将全部文件拷贝到你的目录下。
增加留言¶
增加new_comment()的View方法¶
在前面的模板中我们定义了增加留言的链接:
<a href="{{=url_for('%s.views.new_comment' % request.appname)}}">New Comment</a>
可以看出,我们使用了url_for来生成反向的链接。关于url_for在前面已经讲了,这里要注意的就是 函数名为new_comment,因此我们需要在views.py中生成这样的一个方法。
打开views.py,加入以下代码:
@expose('/new')
def new_comment():
from forms import NoteForm
import datetime
form = NoteForm()
if request.method == 'GET':
return {'form':form, 'message':''}
elif request.method == 'POST':
flag = form.validate(request.values)
if flag:
n = Note(**form.data)
n.save()
return redirect(url_for(index))
else:
message = "There is something wrong! Please fix them."
return {'form':form, 'message':message}
可以看到链接是 /new
。
首先我们导入了NoteForm这个类。它是用来生成录入Form的类,并且可以用来对数据进行校验。一会儿会对它进行介绍。
然后创建form对象。
再根据request.method是GET还是POST来执行不同的操作。对于GET将显示一个空Form,对于POST 表示用户提交了数据,要进行处理。使用GET和POST可以在同一个链接下处理不同的动作,这是一种 约定,一般中读操作使用GET,写或修改操作使用POST。
在request.method为GET时,我们只是返回空的form对象和一个空的message变量。form.html()可 以返回一个空的HTML表单代码。而message将用来提示出错的信息。
在request.method为POST时, 首先调用 form.validate(request.values)
对数据进行校验。
它将返回一个二元的tuple。第一个参数表示成功还是出错,第二个为成功时将转换为Python格式后
的数据,失败时为出错信息。
当flag为True时,进行成功处理。一会我们可以看到在表单中并没有datetime字段,因为我们
手工添加一个值,表示留言提交的时间。然后通过 n = Note(**form.data)
来生成Note记录,但这里并没有提
交到数据库中,因此再执行一个 n.save()
来保存记录到数据库中。
然后执行完毕后,调用 return redirect
进行页面的跳转,跳回留言板的首页。这里又使用了url_for来反
向生成链接。
当flag为False时,进行出错处理。
定义录入表单¶
为了与后台进行交互,让用户可以通过浏览器进行数据录入,需要使用HTML的form系列元素来定义 录入元素。对于有经验的Web开发者可以直接手写HTML代码,但是对于初学者很麻烦。并且你还要考虑 出错处理,数据格式转换的处理。因此许多框架都提供了生成表单的工具,Uliweb也不例外。Form模 块就是干这个用的。
在GuestBook目录下创建forms.py文件,然后添加以下代码:
from uliweb.form import *
class NoteForm(Form):
message = TextField(label='Message:', required=True)
username = StringField(label='Username:', required=True)
homepage = StringField(label='Homepage:')
email = StringField(label='Email:')
这里我定义了4个字段,每个字段对应一种类型。象TextField 表示多行的文本编辑,StringField表示单行文本,你还可以使用象:HiddenField, SelectField, FileField, IntField, PasswordField, RadioSelectField等字段类型。
也许你看到了,这其中有一些是带有类型的,如IntField,那么它将会转换为对应的Python数据类 型,同时当生成HTML代码时再转换回字符串。
每个Field类型可以定义若干的参数,如:
- label 用来显示一个标签
- required 用来校验是否输入,即不允许为空
- default 缺省值
- validators 校验器
很象Model的定义,但有所不同。
编写new_comment.html模板文件¶
在GuestBook/templates下创建new_comment.html,然后添加以下内容:
{{extend "base.html"}}
{{block content}}
{{if message:}}
<p class="warning">{{=message}}</p>
{{pass}}
<h1>New Comment</h1>
<div class="form">
{{<<form}}
</div>
{{end}}
首先是 {{extend "base.html"}}
表示从base.html继承。
然后是一个 if 判断是否有message信息,如果有则显示。这里要注意if后面的’:’号。
然后显示form元素,这里使用了 {{<< form}}
。form是从View中传入的,而{{<<}}
可以对要输出的内容中的HTML标签 不进行转义处理。而 {{=variable}} 将对variable
变量的HTML标签进行转换。因此,如果你想输出原始的HTML文本,要使用{{<<}}来输出。
现在可以在浏览器中试一下了。
删除留言¶
在前面guestbook.html中,我们在每条留言前定义了一个删除的图形链接,形式为:
<a href="{{= url_for('GuestBook.views.del_comment', id=n.id) }}">
那么下面就让我们实现它。
打开GuestBook/views.py文件,然后添加:
@expose('/delete/<id>')
def del_comment(id):
n = Note.get(int(id))
if n:
n.delete()
return redirect(url_for(index))
else:
error("No such record [%s] existed" % id)
删除很简单,首先通过 Note.get(int(id))
来得到对象,然后再调用对象的delete()
方法来删除。
URL参数定义¶
请注意,这里expose使用了一个参数,即 <id>
形式。一旦在expose中的url定义
中有 <type:para>
的形式,就表示定义了一个参数。其中type:可以省略,它可以是int等类型。而
int将自动转化为 \d+
这种形式的正则式。Uliweb内置了象: int, float, path, any, string等类型,你可以在 URL Mapping 文档中了解更多的细节。如果你只定义了
<name>
这种形式,它表示匹配 //
间的内容。一旦在URL中定义了参数,则需要
在View函数中也需要定义相应的参数,因此del_comment函数就写为了: del_comment(id)
。
这里的id与URL中的id是一样的。
好了,现在你可以试一试删除功能是否可用了。
出错页面¶
当程序出错时,你可能需要向用户提示一个错误信息,因此可以使用error()方法来返回一个出错 的页面。它的前面不需要return。只需要一个出错信息就可以了。
那么出错信息的模板怎么定义呢?在你的templates目录下定义一个名为error.html的文件,并加 入一些内容即可。
创建error.html,然后,输入如下代码:
{{extend "base.html"}}
{{block title}}Error{{end}}
{{block header}}<h1>Error!</h1>{{end}}
{{block content}}
<p>{{=message}}</p>
{{end}}
这个页面很简单,就是覆盖了一些block的定义。如title, header, content。
结论¶
经过学习,我们了解了许多内容:
- ORM的使用,包括:ORM的初始化配置,Model的定义,简单的增加,删除,查询
- Form使用,包括:Form的定义,Form的布局,HTML代码生成,数据校验,出错处理
- 模板的使用,包括: {{extend}} 的使用,在模板环境中增加自定义函数,子模板变量定义的 技巧,错误模板的使用,Python代码的嵌入
- View的使用,包括:redirect, error的使用, 静态文件处理
- URL映射的使用,包括:expose的使用,参数定义,与View函数的对应
- 结构的了解,包括:Uliweb的app组织,settings.ini的简单使用,view函数与模板文件 的对应关系
这里演示的View的处理还是基于函数的方式 ,在另一篇 Simple Todo (Uliweb 版本) 之 基础篇 中有如何使用Class方式的View。
Sina Application Engine部署及开发指南¶
Sina Application Engine(简称sae)是新浪发布的类似于GAE的云环境,目前已经支持PHP 和Python。因为sae是一个受限环境,因此uliweb在它上面的部署和开发有一些特殊的地方, 甚至有一些app是专门为sae开发的。下面让我由浅入深地向你介绍sae环境的部署和使用。 因为sae的python环境也在不断完善中,所以本文档也会不断完善。
uliweb的安装¶
你需要安装uliweb 0.1以上版本或svn中的版本,简单的安装可以是:
easy_install Uliweb
安装后在Python环境下就可以使用uliweb命令行工具了。
目前Uliweb支持Python 2.6和2.7版本。3.X还不支持。
Hello, Uliweb¶
让我们从最简单的Hello, Uliweb的开发开始。首先假设你已经有了sae的帐号.
创建一个新的应用,并且选择Python环境。
从svn环境中checkout一个本地目录
进入命令行,切換到svn目录下
创建Uliweb项目:
uliweb makeproject project
会在当前目录下创建一个
project
的目录。这个目录可以是其它名字,不过它是和后面要使用的index.wsgi
对应的,所以建议不要修改。创建
index.wsgi
文件,Uliweb提供了一个命令来做这事:uliweb support sae
这样会在当前目录下创建一个
index.wsgi
的文件和lib
目录。注意执行时是在svn的目录,即project的父目录中。index.wsgi
的内容是:import sae import sys, os path = os.path.dirname(os.path.abspath(__file__)) project_path = os.path.join(path, 'project') sys.path.insert(0, project_path) sys.path.insert(0, os.path.join(path, 'lib')) from uliweb.manage import make_application app = make_application(project_dir=project_path) application = sae.create_wsgi_app(app)
其中
project
和lib
都已经加入到sys.path
中了。所以建议使用上面 的路径,不然就要手工修改这个文件了。然后就可以按正常的开发app的流程来创建app并写代码了,如:
cd project uliweb makeapp simple_todo 这时一个最简单的Hello, Uliweb已经开发完毕了。
如果有静态文件,则需要放在版本目录下,Uliweb提供了命令可以提取安装的app的静态文件:
cd project uliweb exportstatic ../static
如果有第三方源码包同时要上传到sae中怎么办,Uliweb提供了export命令可以导出已经 安装的app或指定的模块的源码到指定目录下:
cd project uliweb export -d ../lib #这样是导出全部安装的app uliweb export -d ../lib module1 module2 #这样是导出指定的模块
为什么还需要导出安装的app,因为有些app不是放在uliweb.contrib中的,比如第三方 的,所以需要导出后上传。但是因为export有可能导出已经内置于uliweb中的app,所以 通常你可能还需要在
lib
目录下手工删除一些不需要的模块。提交代码
访问
http://<你的应用名称>.sinaapp.com
,就可看到项目的页面了。
数据库配置¶
Uliweb中内置了一个对sae支持的app,还在不断完善中,目前可以方便使用sae提供的MySql 数据库。它是需要同时安装sqlalchemy才可以运行的。因此我们第一步先在本地安装好 sqlalchemy,然后在版本目录中,导出sqlalchemy到lib下:
uliweb export -d ../lib sqlalchemy
然后修改 project/apps/settings.ini
在 GLOBAL/INSTALLED_APPS
最后添加:
[GLOBAL]
INSTALLED_APPS = [
...
'uliweb.contrib.sae'
]
然后为了支持每个请求建立数据库连接的方式,还需要添加一个Middleware在settings.ini中:
[MIDDLEWARES]
transaction = 'uliweb.orm.middle_transaction.TransactionMiddle'
TransactionMiddle 不仅是用来控件事务,同时也可以用来控制是否使用短连接。因为 SAE不支持长连接,所以需要在 settings.ini 中启动短连接的配置项:
[ORM]
CONNECTION_TYPE = 'short'
这样就配置好了。而相关的数据库表的创建维护因为sae不能使用命令行,所以要按sae的 文档说明通过phpMyAdmin来导入。以后Uliweb会増加相应的维护页面来做这事。
SAE的受限环境说明¶
具体内容请参见下面的开发文档,因为它是一个受限环境,所以一些常用的使用方式可能有变化,下面列出我写的一些补充:
- 数据库连接不能是长连接,超时时间目录为30s,所以才需要安装db_connection middleware,它就是用来保证每个请求创建数据库连接和关闭数据库连接。
- PIL模块目前还没有预装。PHP环境下的GD还不能用。
- 虽然有临时目录可以写文件,但是os模块不能执行mkdirs,因此无法创建目录。
- 文件上传目录只能使用数据库,sae提供的storage目录还无法使用。
Baidu Application Engine部署及开发指南¶
Baidu Application Engine(简称bae)是百度发布的类似于GAE的云环境,目前提供了对 python的支持。不过现在还要测试过程中,所以以下的部署说明有可能会发生变化。bae 的环境有些和sae(Sina Application Engine)类似,不过还是有一些区别,后面会简单 介绍。本文档假设你已经有了bae的帐号。
Python项目创建¶
为了使用Uliweb你的第一步是要先创建一个Python的项目,目前需要有邀请码并由管理人 员开通。不要问我要邀请码,因为我只有一个,为了做测试已经用掉了。
- 进入 http://developer.baidu.com/service
- 点击百度应用引擎,点击 “立刻开始使用”
- 然后创建应用
- 创建好后,大概是这样:
- 点击版本管理后,新建一个版本。这里bae的版本和sae差不多,都是用数字作为版本 目录。它是从0开始。创建好后,在下面可以看到svn的地址。这样可以用Svn工具checkout 代码了。用户名口令就是百度的用户名口令。
Note
要注意的是,我在使用chrome 21.x dev版本时报了一些js的错,所以后来切換到了 ie上来执行。说是我的chrome版本太高了,不知道算不算bug。
缺省情况下,刚checkout出来的代码只有3个文件:
app.conf favicon.ico index.py
其中index.py是启动文件,这个后面我们要修改它,app.conf就象是GAE的app.yaml的 内容,可以设置 url 的配置信息等。在bae的文档上有更详细的说明。
其实到这里,应用应该已经可以跑起来了。bae已经生成了缺省的环境。点某个版本上的 预览 就可以看到当前版本号执行的效果。
下面就开始 Uliweb 的部署过程。
uliweb的部署¶
uliweb安装¶
这里我不会演示一个 Hello, Uliweb 如何做,太简单了。我会使用 uliwebdoc 下的 SimpleTodo 的例子来做。它的代码你可以从 uliweb-doc 项目 中的 projects中找到,不过这个版本还不能直接运行到 bae 上去,所以需要进行必要的修改。
目前uliweb最新版本是0.1,在发布时还没有对bae作特殊的支持处理。所以你要从svn或github 上下载最新的 uliweb 的源码。点击上面的zip图标 就可以下载了。
然后为了方便在uliweb的解压目录下执行:
python setup.py develop
来安装uliweb。这步是为了在你的机器上创建uliweb环境。
执行bae的支持¶
在git中的最新uliweb版本已经包含了对bae的支持。所以先进入你的项目的某个版本,比如
我的版本目录是: bae/0
然后进入命令行,执行:
uliweb support bae
它会在当前目录下创建 lib 目录。同时会使用uliweb提供的 index.py
覆盖原来的 index.py
文件。同时会覆盖app.conf。让我们分别看一下:
#index.py
import sys, os
path = os.path.dirname(os.path.abspath(__file__))
project_path = os.path.join(path, 'project')
sys.path.insert(0, project_path)
sys.path.insert(0, os.path.join(path, 'lib'))
from uliweb.manage import make_application
application = make_application(project_dir=project_path)
这是启动文件,没什么特殊的。不过上面要注意, index.py
默认是使用 project
作为项
目目录,并且是位于版本目录下面。所以如果你的项目名和结构不同,要对上面 project_path
作必要的修改。
- ::
#app.conf
- handlers:
- url : /?(.*) script : index.py
- expire : .jpg modify 10 years
- expire : .swf modify 10 years
- expire : .png modify 10 years
- expire : .gif modify 10 years
- expire : .JPG modify 10 years
- expire : .ico modify 10 years
这是 app.conf
内容。原来 url 那行就是 ‘/’ ,这里会替換为 /?(.*)
。上面的
格式是yaml的,所以和GAE很象。这个文件也可以直接在界面上进行修改。同时所有的文件
都可以在界面上直接修改。修改后会自动保存到svn中,你可以 update 与本地文件进行合并
处理。这点还是很方便的。
经过上面的处理,我们在 0 目录下已经有了:
lib/
app.conf
index.py
favicon.ico
然后我们要把 uliweb 的压缩包放到 lib
目录下。因为 lib 会自动加入到 sys.path
中。所以我们要保留 uliweb 子目录,以便可以直接导入 uliweb 。这里这样的处理是因为
bae中还没有预装 uliweb 所以我们要自已上传。
然后我们创建 project 目录,把 uliweb-doc/projects/simple_todo 的代码拷贝过来。这样 我们的目录结构基本上就是:
Simple To Do 部署¶
simple to do 是一个简单的todo展示。功能很简单,但是它使用到了数据库。所以使用 这个例子是为了验证数据库的安装是否正确。但是没有使用到session,所以无法测试session 是否可以正常使用。
simple_todo settings.ini修改¶
将simple to do的代码拷过来后,首先要修改 project/apps/settings.ini
[GLOBAL]
DEBUG = False
INSTALLED_APPS = [
'uliweb.contrib.staticfiles',
'uliweb.contrib.orm',
'uliweb.contrib.bae',
'todo',
]
[SITE]
SITE_NAME = '任务跟踪'
EMAIL = 'limodou@gmail.com'
[ORM]
DATABASE = '数据库名'
这里在 uliweb.contrib.orm
后面添加了一行 'uliweb.contrib.bae'
,目的是
可以从 bae.core.const
中获得mysql相关的信息。
我是如何得知 bae.core.const 的呢?是在看到关于常量的描述时提到的,于是我通过:
import bae.core.const
raise Exception(dir(bae.core.ocnst))
来显示,看到有关于 MySQL 的配置项,包括:
MYSQL_HOST
MYSQL_PORT
MYSQL_USER
MYSQL_PASS
MYSQL_DB #不可用
不过发现 MYSQL_DB
得到的值是 None
,所以不知道这是不是一个 BUG 。所以我只能在
settings.ini
中増加了一个 DATABASE
的配置项,所以这就是上面 settings.ini
中
DATABASE
的来源。
这里, uliweb.contrib.bae
会自动依赖 uliweb.contrib.orm
,就是说你不配置 uliweb.contrib.orm
也是可以的,它会自动配置上。 uliweb.contrib.bae
的作用是在执行 uliweb.contrib.orm
之前得到正常的 CONNECTION
串,将放到 settings.ORM.CONNECTION
中,这样 uliweb.contrib.orm
就可以正确处理了。
todo表创建¶
因为 bae 无法使用ssh来 telnet ,所以 uliweb 提供的命令行就无用了。原本启动前的
第一步就是执行 syncdb
来创建相关的表。但是执行不了,所以只能使用 phpadmin 来处理
了。所以首先在命令行下执行:
uliweb sql
它会打印出建表的SQL语句,拷贝下来到phpadmin中执行它吧。不过 uliweb sql 无法打印 含有创建 Index 的语句。所以如果存在索引创建,要自已手工去使用 phpadmin 来建。
启动¶
将修改后的代码每次一提交,bae 会自动启动应用。使用预览的话可以看到每个版本的情况。 如果执行正确,在应用面板中,对于相要生效的版本点击前面的 radio button。bae会提 示你生效,一旦生效。你就可以通过域名来访问了。
uliweb的演示版本为: http://0.uliweb.duapp.com/ 有兴趣可以试试。下面为页面示例:
BAE的受限环境说明¶
具体的受限说明详见下面的开发文档。特别要说明的是上面的 settings.ini
中的 DEBUG ``为 ``False
。
为什么,因为bae把 wsgi['stderr']
给禁掉了。而 uliweb 的 debug 功能是依赖于 werkzeug
的 DebugApplication
类,它要使用 wsgi['stderr']
的。因为写不进去,所以就会报错。
不知道这块为什么要禁掉,这样调试就会很麻烦。
在bae的预装软件列表中,已经有了 uliweb 要使用的数据库相关的包,如:sqlalchemy, mysql-python。 其中 Werkzeug 不是必须的,虽然它已经装了,不过 uliweb 自带一个并且是有所修改的版本。
BAE的开发文档¶
感受¶
BAE 和 SAE 为国内提供了云部署环境,支持 Python 很给力,不过限制比起我用过的 dotCloud等国外的环境还是有些多了点,比如 dotCloud 提供了命令行工具,可以 ssh 登录服务器,这样安装调试都很方便。并且没有太多环境的限制。希望以后在这些方便 可以加强。
Heroku 部署说明¶
Heroku本为提供Ruby云环境的平台,不过现在也支持 Python ,下面将描述如何在它上面
部署 Uliweb。此应用以 simple_todo 为例,可以从 uliweb-doc/projects
中得到。
客户端下载及登录¶
首先保证你已经有了 heroku 的帐号。然后去它的客户端页面下载合适的客户端,这里我 以windows客户端为例。下载并安装后,首先执行:
heroku login
根据提示输入你的邮箱和密码,然后它会自动查找本地的ssh公钥,如果找到,则会问你是 否要上传。因为我以前已经安装过 git 所以已经生成过公钥。
创建项目¶
首先在本地创建一个目录作为你的项目目录,如: herodu。进入这个目录,然后执行:
heroku create --stack cedar
执行如图:
我们可以从结果中看到已经生成了可访问的URL和git仓库地址。在后面我们可以改个名。 不过我试了,如果还没有上传过代码,好象改不了。
文本没有使用virtualenv来创建 Python 环境,所以省略了这块的处理。
编写代码¶
首先保证你的uliweb的版本在 0.1.1 以上,因为从 0.1.1 才支持 heroku 。在项目目录 下执行:
uliweb support heroku
这样会在当前目录下生成:
lib/
.gitignore
app.py
Procfile
requirements.txt
其中:
- lib/
- 用来存放你想上传的一些库。不过在一般情况下,你可以通过 requirements.txt 来安装,因此不一定需要。不过在 app.py 中会将 lib 目录添加到 sys.path 中。
- .gitignore
- 因为 heroku 使用的是 git 上传版本,所以这里提供了一个简单的 .gitignore 文件, 你可以根据需要进行修改。
- app.py
- 启动文件。它会自动识别
os.environ['PORT']
如果没有缺省使用 5000 端口, 同时将 IP 绑定到0.0.0.0
上。 - Procfile
Heroku 会采用 foreman 来启动应用,你可以在本地用它来启动 uliweb 项目。访问 地址可以是
http://localhost:5000
Note
因为我更熟悉nginx+uwsgi的模式,因此使用foreman的话不知道有没有性能问题。 并且没有看到如何配置静态文件?所以在我的试验中能用开发服务器来提供服务和 静态文件的支持。
- requirements.txt
pip 安装需求文件,缺省要安装:
Uliweb==0.1.1 psycopg2==2.4.5 SQLAlchemy==0.7.7
因为 heroku 采用的是 postgreSql 数据库。
然后将 simple_todo (在uliweb-doc/projects/simple_todo下) 的代码拷贝到项目目录下。 注意,这里没有单独创建一个project的项目目录,而是直接使用项目目录。所以在这个目 录下应该直接有 apps 子目录。当前目录结构如:
因为 heroku 可以直接运行后端的命令,因此就可以直接运行 uliweb 的命令行工具,如 同步数据库。所以将项目目录直接做为uliweb项目是为了方便执行命令行。
接着修改 apps/settings.ini
,内容为:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
'uliweb.contrib.staticfiles',
'uliweb.contrib.orm',
'uliweb.contrib.heroku',
'todo',
]
[SITE]
SITE_NAME = '任务跟踪'
EMAIL = 'limodou@gmail.com'
在Heroku是可以使用 Debug 调试的。上面在 uliweb.contrib.orm
后面安装了 uliweb.contrib.heroku
。
在 uliweb.contrib.heroku
中可以自动从 os.environ
中获得 DATABASE_URL
的信息。
上面的代码基本上写好了。然后我们直接上传。
上传代码¶
首先在本地创建GIT创建,把所有代码提交进去:
git init
git add .
git commit -m "message"
然后检查一下remote中是不是有heroku:
git remote
如果没有则将上面的git地址添加到remote中去:
git remote add heroku <heroku地址>
然后开始push代码:
git push heroku master
在push成功时,会根据相关的requirements.txt等信息开始后台的部署和服务启动。但是 此时很有可能数据库还未生效。前面已经说了,uliweb.contrib.heroku 会从环境变量中 获得数据库连接的串,而uliorm所使用的数据库连接和 heroku 提供的是一致的。这块都 已经由这个 app 处理了。现在的关键就是要创建数据库的实现。
创建数据库¶
可以先用:
heroku config
结果如图:
可以看到有 DATABASE_URL
的信息。如果没有,说明数据库实例没有创建,则可以执行:
heroku addons:add shared-database
应用改名¶
前面说了,如果觉得自建的应用名不好看,可以修改一下,使用:
heroku rename <你想改的名字>
一旦名字修改了,你的服务URL和git的地址都将发生变化。不过服务地址会自动变化,所以 不用做什么特殊处理。
数据库表的同步¶
代码传完了,名字也改了,数据库也建了,但是访问 simple_todo 还是有问题,因为这个 例子需要创建一个 todo 的表。heroku没有提供phpadmin,不过它可以在本地执行远端的 命令,所以很方便,下面执行:
heroku run uliweb syncdb
这里你会看到 todo 表被创建的信息。这样就可以访问你的应用了。我的例子是 http://uliweb.herokuapp.com/
技术文档:
Settings说明¶
调用说明¶
在Uliweb中,配置信息一般是放在settings.ini文件中的,目前有以下几个级别的settings.ini 信息:
default_settings.ini
apps/
app/settings.ini
settings.ini
local_settings.ini
从上往下看,我们可以看到有缺省的settings信息,也有app级别的settings信息,还有整 个项目的settings信息,最后是项目的本地settings信息。它们的加载顺序是从上到下, 因此,后加载的项一旦发现有重名的情况,会进行以下特殊的处理:
如果值为list, dict,则进行合并处理,即对于list,执行extend(),对于dict执行update
如果是其它的值,则进行替換,即后面定义的值覆盖前面定义的值
如果写法为:
name <= value
则不管value是什么都将进行替換。
settings.ini格式¶
settings.ini的写法是类ini格式,但是和标准的ini有所区别:
基本写法是:
[section] name = value
其中section是节的名字,可以是一般字符串或标识符。大小写敏感。
- name
是key,不应包含 ‘=’
- value
是值,并且是符合python语法的,因此你可以写list, dict, tuple, 三重字符串和 其它的标准的python的类型。可以在符合python语法的情况下占多行。
不支持多级,只支持两级处理
注释写法是第一列为
'#'
可以在文件头定义
#coding=utf-8
来声明此文件的编码,以便可以使用u'中文'
这样的内容。可以使用
_('English')
这样的编译字符串,_()函数将被定义为 ugettext_lazy可以使用
[include:/project/local.ini]
这样的声明,用来在当前位置引入外部的 ini 文件。
settings的处理是由Uliweb在启动时按照以上的顺序自动解析并且合并的。对于app下的settings.ini文件,它会按照app的定义顺序来处理。
pyini模块¶
整个settings的处理都是由pyini模块来处理的。它存在于uliweb/utils目录下,在程序中 的使用如:
from uliweb.utils import pyini
x = pyini.Ini('inifile')
这样就可以打开一个ini文件。如果有多个ini文件要进行合并处理,则使用 read()
继续处理即可,如:
x.read('another.ini')
settings.ini文件可以保存,如:
x.save()
settings的使用¶
settings在读取后会生成一个对象,它有几种使用方式:
settings[section][key]
以字典的形式来处理settings.get_var('section/key', default=None)
这种形式可以写一个查找的路径的形式settings.section
或settings.section.key
以.
的形式来引用section和key,不过要求section和key是标识符,且不能是保留字。for k, v in settings.section.items()
可以把settings.section当成一个字典来使用,因此字典的许多方法都可以使用,如 in, has 之类的。
在views方法中,可以直接使用settings对象。在非view方法中,可以先导入再使用:
from uliweb import settings
重要配置参数说明¶
以下按不同的节(section)来区分
GLOBAL¶
[GLOBAL]
TEMPLATE_SUFFIX = '.html' #模板文件后缀
ERROR_PAGE = 'error' + TEMPLATE_SUFFIX #错误页面文件名
WSGI_MIDDLEWARES = [] #WSGI中间件
HTMLPAGE_ENCODING = 'utf-8' #页面文件编码
FILESYSTEM_ENCODING = None #文件系统编码,在linux上,建议进行相应的设置
DEFAULT_ENCODING = 'utf-8' #缺省编码
TIME_ZONE = None #时区设置
LOCAL_TIME_ZONE = None #本地时区
TEMPLATE_TEMPLATE = ('%(view_class)s/%(function)s', '%(function)s')
#不同view方法的模板路径形式,前者为类形式,
#后者为函数形式
TEMPLATE_DIRS = [] #全局的模板路径,当所有app中都找不到模板时,将在
#这个目录下进行查找
其中 TEMPLATE_TEMPLATE
用于对应不同的view形式的模板路径方式。对于类,缺省是
在templates下为 classname/function.html
的形式。而函数形式的view则直接对应
templates下的 function.html
。
FUNCTIONS¶
用于定义公共的一些函数,例如:
[FUNCTIONS]
flash = 'uliweb.contrib.flashmessage.flash'
在此定义之后,可以有以下两种引用形式:
from uliweb import function
flash = function('flash')
flash(message)
#或
from uliweb import functions
functions.flash(message)
DECORATORS¶
用于定义公共的一些decorator函数,类似于FUNCTIONS的使用方式,但是区分为全部是decorator。
使用形式为:
from uliweb import decorators
@decorators.check_role('superuser')
def index():
pass
BINDS¶
用于绑定某个信号的配置,例如:
[BINDS]
audit.post_save = 'post_save'
在配置中,每个绑定的函数应有一个名字,在最简单的情况下,可以省略名字,函数名就 与绑定名相同。
BINDS有三种定义形式:
function = topic #最简单情况,函数名与绑定名相同,topic是对应的信号
bind_name = topic, function #给出信号和函数路径
bind_name = topic, function, {kwargs} #给出信号,函数路径和参数(字典形式)
其中function中是函数路径,比如 appname.model.function_name
,例用这种形式,uliweb
可以根据 appname.model
来导入函数。
EXPOSES¶
用于配置URL,在一般情况下,你只要在views.py中定义 @expose(url)
即可,但是在复杂情况
下,特别是可以允许URL被替换的情况下,考虑把URL定义在settings.ini中,如:
[EXPOSES]
login = '/login', 'uliweb.contrib.auth.views.login'
logout = '/logout', 'uliweb.contrib.auth.views.logout'
URL在Uliweb中是可以给每个URL起个名字的,以便在反向获取时只使用这个名字,同时它也可以用来方便进行替換。
它也有三种定义方式,类似于BINDS的定义:
function = url #最简单情况,函数名与url名相同
url_name = url, function #给出url, 函数路径和url名
url_name = url, function, {kwargs} #给出url,函数路径,url名和参数(字典形式)
I18N 使用说明¶
Uliweb支持标准的gettext的i18n的处理。在程序中和在模板中都可以使用,甚至在ini文件 中也可以使用。
I18N的配置¶
为了使用i18n,需要在settings.ini中的INSTALLED_APPS中添加 'uliweb.contrib.i18n'
,
同时uliweb.contrib.i18n下的settings.ini还定义了一些配置项,如:
[I18N]
LANGUAGE_COOKIE_NAME = 'uliweb_language'
LANGUAGE_CODE = 'en'
LOCALE_DIRS = []
SUPPORT_LANGUAGES = []
LANGUAGE_COOKIE_NAME
是用来将语言的选择存在cookie中使用的,一般不需要修改。LANGUAGE_CODE
是表示缺省的语言,缺省为英文。LOCALE_DIRS
可以用来设置语言文件所在的目录,可以是多个。同时在其中还可以定 义一些特殊的变量,如:${PROJECT}
表示项目目录,${plugs}
表示plugs模块所在的目录。 所以通过这些变量可以引用一些与项目有关的相对路径。SUPPORT_LANGUAGES
表示网站所支持的语言种类。除原本开发所使用的语言外,如果浏 览器自动希望某种语言,并且这种语言在SUPPORT_LANGUAGES
所支持的范围之内时,才会 生效。
一旦配置了I18N,则会自动应用 'uliweb.i18n.middle_i18n.I18nMiddle'
这个middleware,
这个可以在uliweb.contrib.i18n/settings.ini中看到。
I18N语言的判定规则¶
在Uliweb中,可以有许多种方式来决定使用哪种语言,比如:在session中,在cookie中,
通过浏览器发送的HTTP头来自动判断,还有就是在settings中的 LANGUAGE_CODE
来决定。
Uliweb在处理时按照以下的顺序来决定是否使用某种语言:
如果url的Query_String上有
lang=xxx
的信息,则这个值为要使用的语言。 (0.0.1増加)Note
lang
的名字是可以在settings.ini中配置的,缺省为空,需要设置此功能才会启动。 配置选项如下:[I18N] URL_LANG_KEY = 'lang'
如果session中存在 uliweb_language 的定义,则这个值为要使用的语言;
如果cookie中存在与settings.ini中
I18N/LANGUAGE_COOKIE_NAME
对应的值时,则这个 值为要使用的语言;如果浏览器发送的HTTP头有
HTTP_ACCEPT_LANGUAGE
的信息,并且得到的语言在SUPPORT_LANGUAGES
中有定义,则使用HTTP头中定义的语言;最后选择settings.ini中定义的
LANGUAGE_CODE
所以如果你的网站支持多种语言,可以按照上面的说明来选择不同的模式。简单情况下只
要定义 LANGUAGE_CODE='zh_CN'
就可以支持中文了。如果想支持浏览器自动识别,可以定义
settings.ini为:
[I18N]
LANGUAGE_CODE = 'en'
SUPPORT_LANGUAGES = ['en', 'zh_CN']
上面的定义说明当前网站支持两种语言,缺省为 'en'
。
I18N字符串的定义¶
为了使用i18n,首先要在程序中进行i18n的定义,根据文件格式的不同,使用略有不同。
程序文件¶
这里是指.py文件。示例如下:
from uliweb.orm import *
from uliweb.i18n import ugettext_lazy as _
class User(Model):
username = Field(str, verbose_name=_('Username'), max_length=30, unique=True, index=True, nullable=False)
email = Field(str, verbose_name=_('Email'), max_length=40)
password = Field(str, verbose_name=_('Password'), max_length=128, nullable=False)
在uliweb.i18n中定义了几组gettext函数,常用的就是gettext, ugettext, gettext_lazy 和ugettext_lazy。带u的表示返回为unicode字符串,否则为字符串。后缀有lazy的表示 在运行时会根据当时的上下文环境自动进行语言的切换。因此一般使用带有lazy的函数。
Note
使用lazy的函数不能支持 _('%s') % 'name'
这种形式,所以建议使用非lazy的函数
模板¶
因为uliweb的模板会编译为.py文件,所以除了不用导入gettext函数外,和在程序中无差别。
一旦你配置了i18n的app,会自动向模板环境中添加 _()
函数。示例:
<div>hi, {{=_('name')}}</div>
所以要编辑的串,都要放在 {{}}
中,并且使用 _()
进行处理。
Attention
在模板中 _()
中的字符串不要使用 u''
的形式,并且整个模板以utf-8来保存。
I18N字符串的提取¶
uliweb.contrib.i18n App提供了一个i18n的命令它提供以下参数:
--apps | 如果设置,将从存在于项目中的所有app中进行抽取,并将 .po 文件保存在每个app下的 locale 目录中。 |
-p | 如果设置,将从项目目录抽取翻译信息。 |
-d DIRECTORY | 如果设置,将从指定目录抽取翻译信息。 |
--uliweb | 如果设置,将从uliweb包中抽取翻译信息。 |
-l LOCALE | 目标语言,缺省是 en 。 |
--exact | 如果设置,则在旧的.po中存在,但是在.pot中不存在的项目将被删除。 (0.0.1新増) |
-t TEMPLATE, --template=TEMPLATE | |
PO文件中一些静态信息的定义,如:charset, translater等模板的定义。 |
因此i18n支持几种语言提取方式:
--apps
这种方式会在每个app下都生成一个locale的目录。注意这种方式只处理存在于项目目录下的app,对于已经安装但是不存在于项目目录下的app将不做处理。-p
这种方式会将项目下创建一个locale目录,将整个项目所有的内容都放在一个文件中-d
按指定目录进行处理。比如plugs它只是一个app的集合,并不是一个完整的项目,所以 上述参数无法使用,但是可以使用这个-d参数进行处理--uliweb
对uliweb本身进行处理。因为uliweb中有些文件中也有i18n的翻译串,所以 可以使用这个命令来处理-t
它允许你写一个po的模板文件,在uliweb.i18n下已经提供了一个示例,名为:po_template.ini
其内容为:[I18N] First_Author = 'FIRST AUTHOR <EMAIL@ADDRESS>' Project_Id_Version = 'PACKAGE VERSION' Last_Translator = 'FULL NAME <EMAIL@ADDRESS>' Language_Team = 'LANGUAGE <LL@li.org>' Content_Type_Charset = 'utf-8' Content_Transfer_Encoding = '8bit' Plural_Forms = 'nplurals=1; plural=0;'
你可以修改其中的内容,它们将自动填充到生成的pot和po文件中。
使用示例:
uliweb i18n -p -l zh_CN
注意, -l
参数如果不提供则自动为 'en'
。因此,如果要翻译中文一定要加上 -l zh_CN
。
编码注意事项¶
在i18n中提供gettext和ugettext,它们的区别是:前者返回字符串,后者返回unicode。 为了正确处理中文编码,建议:程序、模板包括ini文件都使用utf-8编码来处理。
Mail 处理¶
Uliweb中内置了邮件处理,目前支持普通的smtp和gmail的发送模式。如果有特殊要求也 可以自定义邮件发送的后端处理。
配置¶
在settings.ini添加 ‘uliweb.contrib.mail’ app。
同时有以下几项缺省设置:
[MAIL]
HOST = 'localhost'
PORT = 25
USER = ''
PASSWORD = ''
BACKEND = 'uliweb.mail.backends.smtp'
前四项不用过多解释,最后一项是指示使用哪个发送后端。Uliweb已经内置了两个后端, 缺省为smtp方式。
根据使用要求,设置基本上分三种模式。
用户验证模式¶
填写USER, PASSWORD内容,BACKEND为smtp,这种情况下将进行用户验证处理。如:
[MAIL]
HOST = 'localhost'
PORT = 25
USER = 'user'
PASSWORD = 'password'
Gmail模式¶
Gmail有自已特殊的方式,并且它的主机和端口都是特殊的,因此,保持HOST为’‘,PORT为0, 同时填写USER和PASSWORD,BACKEND改为’uliweb.mail.backends.gmail’。如:
[MAIL]
HOST = ''
PORT = 0
USER = 'user'
PASSWORD = 'password'
BACKEND = 'uliweb.mail.backends.gmail'
邮件发送¶
配置好之后就是邮件发送处理。举例如下:
def mail():
from uliweb.mail import Mail
Mail().send_mail('limodou@gmail.com', 'limodou@gmail.com',
u'中文标题', u'中文内容')
return 'ok'
首先是要创建Mail的实例,然后调用send_mail来发送邮件。
其中,如果Mail()没有传入参数时,将自动使用settings中的配置信息。你也可以直接传 入象:host, port, user, password, backend等参数。
send_mail的原型为:
send_mail(from_, to_, subject, message, html=False, attachments=None)
from_
为发信人to_
为收信人subject
为主题message
为内容html
标识是否为HTML内容。如果为True,则message应为HTML片段attachments
为附件,它是一个tuple或list。
如果主题或内容有中文,建议使用unicode或utf-8编码。
本机测试¶
如果你没有环境怎么办,Python提供了这样的方法,在本机命令行执行:
python -m smtpd -n -c DebuggingServer localhost:1025
这样就启动了一个测试用的smtp服务器,将settings.ini中的HOST和PORT修改成: localhost和1025就可以在命令行下看到发送的文本了。不过要看效果的话可能还是要真正 给自已发个邮件才行。
再有就是,如果你可以直接Internet,可以使用上面的gmail的配置来测试。
部署指南¶
uliweb支持任何标准的wsgi方式的部署。缺省情况下,在创建一个项目后,会在项目目录
下生成: fast_handler.fcgi
和 wsgi_handler.py
.
并且uliweb目录还支持:gae, sae和dotcloud。因此,如果想要在这些环境上部署,一般 需要执行:
uliweb support [gae|sae}dotcloud]
则会分别生成相应的环境包含部署用的处理程序。
并且uliewb还提供了静态文件的提取功能,它可以把所有安装的app下的static目录汇总到 指定的目录下去,然后利用web server来提供静态文件的处理。如使用:
uliweb exportstatic /your/static/path
Apache¶
mod_wsgi¶
按 mod_wsgi 的说明安装mod_wsgi到apache下。
- 拷贝mod_wsgi.so到apache的modules目录下
Window环境可以看:
Linux环境可以看:
配置 apache 的httpd.conf文件
LoadModule wsgi_module modules/mod_wsgi.so WSGIScriptAlias / /path/to/uliweb/wsgi_handler.py <Directory /path/to/youruliwebproject> Order deny,allow Allow from all </Directory>
上面是将起始的URL设为/,你可以根据需要换为其它的起始URL,如/myproj。
如果在windows下,示例为:
WSGIScriptAlias / d:/project/svn/uliweb/wsgi_handler.py <Directory d:/project/svn/uliweb> Order deny,allow Allow from all </Directory>
重启apache
测试。启动浏览器输入: http://localhost/YOURURL 来检测你的网站可否可以正常访问。
Lighttpd + SCGI¶
配置lighttpd.conf:
scgi.server=( "/uliweb.scgi"=> ( "main" => ( "socket" => "/tmp/uliweb.sock", "check-local" => "disable", ), ), ) url.rewrite-once = ( "^(/.*)$" => "/uliweb.scgi$1", )
运行:
python runcgi.py protocol=scgi socket=/tmp/uliweb.sck method=threaded daemonize=true
Note
runcgi.py需要使用flup,下地址:http://trac.saddi.com/flup
IIS + SCGI¶
下载安装pyISAPI_SCGI 地址: http://code.google.com/p/pyisapi-scgi/
pyISAPI_SCGI配置方法 http://code.google.com/p/pyisapi-scgi/wiki/PyISAPI_SCGI_0_6_17
编辑scgi.conf:
port=3033 #设置一个空闲的端口号
运行:
python runcgi.py protocol=scgi host=127.0.0.1 port=3033 method=threaded
Note
runcgi.py需要使用flup,下地址:http://trac.saddi.com/flup
虚拟主机(DreamHost,BlueHost,HostMonsger等)¶
FastCGI¶
安装python, 参考http://wiki.dreamhost.com/Python
新建dispatch.fcgi,内容:
#!/home/yourname/bin/python (你安装的python的路径) import sys from runcgi import run run(method='threaded',protocol='fcgi')
编辑.htaccess,内容:
Options +FollowSymLinks +ExecCGI RewriteEngine On RewriteBase / RewriteRule ^(dispatch\.fcgi/.*)$ - [L] RewriteRule ^(.*)$ dispatch.fcgi/$1 [L] AddHandler fastcgi-script .fcgi #或者是AddHandler fcgid-script .fcgi
CGI¶
安装python, 参考http://wiki.dreamhost.com/Python
修改runcgi.py,将第一行内容修改为:
#!/home/yourname/bin/python (你安装的python的路径)
修改.htaccess,内容:
Options +FollowSymLinks +ExecCGI RewriteEngine On RewriteBase / RewriteRule ^(runcgi\.py/.*)$ - [L] RewriteRule ^(.*)$ runcgi.py/$! [L] AddHandler cgi-script .py
Note
以CGI方式运行,需flup 1.0以上版本。
Nginx+uwsgi¶
Nginx¶
使用Nginx运行Uliweb,可以考虑使用nginx+uwsgi的模式,其中nginx采用反向代理的方式 来配置。uwsgi可以采用手工处理,也可以考虑使用supervisor来管理。
在Nginx中配置比较简单,在nginx.conf中添加:
http {
#...
include /etc/nginx/conf.d/*.conf;
#...
}
这里将其它不涉及的内容忽略掉了。上面的代码的作用是包含在conf.d目录下的所有.conf文件。
因此,你可以把特别的配置写到conf.d目录下的某个文件,如: uliweb.conf
。内容
可以是:
server {
listen 80;
location / {
include uwsgi_params;
#proxy_pass localhost:8000;
uwsgi_pass unix:///tmp/uwsgi.sock;
}
}
将uwsgi设置为反向代理有两种方式,一种是通过服务的方式,即上面注释掉的那一行,但 是当请求过多,这种方式会报错。因此一般都采用socket文件的方法。
上面就把Nginx设置好了。如果要使用Nignx提供静态文件服务,可以在上面的server中添加:
location ~ ^/static/ {
root /your/path/to/static;
}
这样就将/static作为静态文件的起如目录了。
uwsgi¶
uwsgi可以支持命令行方式启动,也可以由supervisor来管理(个人以为supervisor要简单 得多)。下面是一个命令行启动uwsgi的一个示例脚本(start.sh):
#!/bin/bash
sockfile=/tmp/uwsgi.sock
projectdir=/your/project/path
logfile=/opt/web/logs/uwsgi.log
if [ $1 = start ];then
psid=`ps aux|grep "uwsgi"|grep -v "grep"|wc -l`
if [ $psid -gt 2 ];then
echo "uwsgi is running!"
exit 0
else
uwsgi -s $sockfile --chdir $projectdir -w wsgi_handler -p 10 -M -t 120 -T -C -d $logfile
fi
echo "Start uwsgi service [OK]"
elif [ $1 = stop ];then
killall -9 uwsgi
echo "Stop uwsgi service [OK]"
elif [ $1 = restart ];then
killall -9 uwsgi
uwsgi -s $sockfile --chdir $projectdir -w wsgi_handler -p 10 -M -t 120 -T -C -d $logfile
echo "Restart uwsgi service [OK]"
else
echo "Usages: sh start.sh [start|stop|restart]"
fi
开始的三个变量可以根据你的实际情况进行修改。这个命令提供了启动、停止、重启三个 功能。并且相应的参数你可以根据情况进行设置。因为uwsgi有许多的参数可以使用,并 且配置参数可以有三种提供方式,如:
- 命令行参数
- ini文件
- xml文件
另外还可以使用supervisor来管理uwsgi程序,如下面是一个示例:
[program:uwsgi]
command = uwsgi
--socket /tmp/uwsgi.sock
--harakiri 60
--reaper
--module wsgi_handler
--processes 2
--master
--home /python/env
--logto /tmp/uwsgi.log
--chmod-socket=666
--limit-as 256
--socket-timeout 5
--max-requests 2
directory=/path/to/yourproject
stopsignal=QUIT
autostart=true
autorestart=true
stdout_logfile=/tmp/supervisord.log
redirect_stderr=true
exitcodes=0,1,2
这里把其它的配置都忽略掉了,只显示uliweb相关的配置,上面的许多参数可以根据要求 进行修改。
其中 --home xxx
的作用是设置python环境,它主要是用于使用virtualenv来创建
python环境的情况。
然后使用supervisorctl就可以进行管理了。
命令行工具使用指南¶
uliweb¶
当运行不带参数的uliweb命令时,会显示一个帮助信息,但是因为命令很多,所以这个帮 助只是列出可用命令的清单,如:
Usage: uliweb [global_options] [subcommand [options] [args]]
Global Options:
--help show this help message and exit.
-v, --verbose Output the result in verbose mode.
-s SETTINGS, --settings=SETTINGS
Settings file name. Default is "settings.ini".
-L LOCAL_SETTINGS, --local_settings LOCAL_SETTINGS
Local settings file name. Default is
"local_settings.ini".
--project PROJECT Project "apps" directory.
--pythonpath PYTHONPATH
A directory to add to the Python path, e.g.
"/home/myproject".
--version show program's version number and exit
Type 'uliweb help <subcommand>' for help on a specific subcommand.
Available subcommands:
call
develop
export
exportstatic
find
i18n
makeapp
makecmd
makepkg
makeproject
runserver
shell
support
在uliweb中,有一些是全局性的命令,有一些是由某个app提供的命令。因此当你在一个 project目录下运行uliweb命令时,它会根据当前project所安装的app来显示一个完整的 命令清单。上面的示例只显示了在没有任何项目时的全局命令。比如你安装了orm app,则 可能显示的清单为:
Available subcommands:
call
createsuperuser
dbinit
develop
droptable
dump
dumptable
dumptablefile
export
exportstatic
find
i18n
load
loadtable
loadtablefile
makeapp
makecmd
makepkg
makeproject
reset
resettable
runserver
shell
sql
sqldot
support
syncdb
其中象 dump*, load*, sql*, syncdb, reset* 等命令都是由orm app提供的。
如果想看单个命令的帮助信息,可以执行:
#> uliweb help sql
Usage: uliweb sql <appname, appname, ...>
Display the table creation sql statement. If no apps, then process the whole dat
abase.
常用全局选项说明¶
除了命令,uliweb还提供了全局可用的参数,它与单个命令自已的参数不同,它是对 所有命令都可以使用的参数。
--help | 和不带任何参数一样,显示帮助信息。 |
-v, --verbose | 是否冗余方式输出。缺省情况下,许多命令在成功时不会有输出结果,只在出错时 显示出错信息。使用-v模式,可以在执行时显示一些执行信息。 |
-s SETTINGS, --settings=SETTINGS | |
uliweb的全局配置文件缺省为project/apps目录下的settings.ini文件,你也可以通过 本参数将其改为其它的文件名。 | |
-L LOCAL_SETTINGS, --local_settings=LOCAL_SETTINGS | |
除全局配置文件外,还有一个本地配置文件,缺省文件名为local_settings.ini。也 同样放在project/apps目录下。它会在settings.ini被处理完之后被处理。 因此,你可以在其中加入只在当前环境有效的参数,并且不将其放入版本库中,以实 现,不同的环境有不同的配置信息。简单讲,可以在settings.ini中放在公共的配置 信息,在local_settings.ini中放入与环境相关的配置信息。 | |
--pythonpath=PYTHONPATH | |
设置python路径,将会被添加到sys.path。 | |
--version | 显示当前的版本 |
runserver¶
启动开发服务器:
Usage: uliweb runserver [options]
参数说明:
-h HOSTNAME | 开发服务器的地址,缺省为localhost |
-p PORT | 开发服务器端口,缺省为8000 |
--no-reload | 是否当修改代码后自动重新装载代码,缺省为True。 |
--no-debug | 是否当出现错误时可以显示Debug页面,缺省为True。 |
--thread | 是否使用线程模式。缺省为False。 |
--processes=PROCESSES | |
启动时创建进程的个数。此命令在windows下不可用。因为它要使用os.fork来创 建进程。 | |
--ssl | 是否启动https模式,需要安装pyOpenSSL,可以通过easy_install pyOpenSSL来安装。它与下面的ssl-key和ssl-cert是要一起使用的。 |
--ssl-key | ssl-key是指明要使用的ssl私钥文件,缺省为当前目录下的ssl.key文件。 |
--ssl-cert | ssl-cert是指明要使用的ssl语书文件,缺省为当前目录下的ssl.cert文件。 |
Note
在werkzeug的文档中有如何生成key和cert文件的方法,示例如下:
$ openssl genrsa 1024 > ssl.key
$ openssl req -new -x509 -nodes -sha1 -days 365 -key ssl.key > ssl.cert
其中在生成cert文件时,会提许多的问题,按要求回答一下就好了。在windows下我是装了git环境,它带了一个openssl的工具,用起来很方便。
示例:
uliweb runserver #启动缺省服务器
export¶
将已安装的app目录下的文件导出到指定目录。它的作用是当部署到某些受限环境时,需要 将用到的模块源码打包上传,通过这个命令可以导出uliweb项目中已经安装的模块的源码, 未安装的app源码将不会导出。同时也可以导出指定模块的源码,如导出uliweb的源码。
Usage: uliweb export [options] [module1 module2]
参数说明:
-d OUTPUTDIR | 将指定的模块源码导出到指定的目录下。 |
示例:
uliweb export -d ../lib
#将所有已安装的app导出到 ``../lib`` 目录下,不包含 static 目录。
uliweb export -d ../lib uliweb
#uliweb包导出到 ``../lib`` 目录下。
Attention
export命令需要在project目录下运行。
exportstatic¶
将所有已安装的app下的static文件和子目录复制到一个统一的目录下。注意,如果你在apps的 settings.py中设定了INSTALLED_APPS参数,则所有设定的app将被处理,如果没有设置,则 按缺省方式,将apps目录下的所有app都进行处理。对于存在同名的文件,此命令缺省将进行检 查,如果发现文件名相同,但内容不同的文件将会给出指示,并且放弃对此文件的拷贝。可以 在命令行使用-no-check来关闭检查。
Usage: uliweb exportstatic [options] outputdir [app1, app2, ...]
参数说明:
-c, --check | 是否在拷贝时进行检查,一旦发现不符会在命令行进行指示。如果设定为 不检查,则直接进行覆盖。缺省为不检查。 |
--js | 和下面的-J连用,用于将js文件进行压缩处理。 |
-J JS_COMPRESSOR | |
JS压缩程序(Jar包)路径。缺省使用Google Clource Compiler(compiler.jar)来 进行处理。默认是从命令执行目录下查找compiler.jar包。 | |
--css | 和下面的-C连用,用于将css文件进行压缩处理。 |
-C CSS_COMPRESSOR | |
CSS压缩程序(Jar包)路径。缺省使用Yahoo的Yui CSS Compressor(yuicompressor.jar) 来进行处理。默认是从命令执行目录下查找yuicompressor.jar包。 |
可以在输出路径后面再指定若干个app的名字,这样只会导出指定app的静态文件。
示例:
uliweb exportstatic static
#将所有已安装的app下的static文件拷贝到static目录下。
find¶
查找对象,包括:模板、URL对应的view、静态文件和Model定义的模块
Usage: uliweb find -u url
or
Usage: uliweb find -t template
or
Usage: uliweb find -c static
or
Usage: uliweb find -m model_name
makeproject¶
生成一个project框架,它将自动按给定的名字生成一个project目录,同时包含有初始子目录和文件。
Usage: uliweb makeproject projectname
示例:
uliweb makeproject project
创建project项目目录。
makeapp¶
生成一个app框架,它将自动按给定的名字生成一个app目录,同时包含有初始子目录和文件。
Usage: uliweb makeapp appname
示例:
uliweb makeapp Hello
创建Hello应用。如果当前目前下有apps目录,则将在apps目录下创建一个Hello的目录, 并带有初始的文件和结构。如果当前目前下没有apps目录,则直接创建Hello的目录。
makecmd¶
向指定的app或当前目录下生成一个commands.py模板。
Usage: uliweb makecmd [appname, ...]
示例:
uliweb makecmd Hello
i18n¶
i18n处理工具,用来从项目中提取_()形式的信息,并生成.pot文件。可以按app或全部app或整个
项目为单位进行处理。对于app或全部app方式,将在每个app下创建: app/locale/[zh]/LC_MESSAGES/uliweb.pot
这样的文件。其中[zh]根据语言的不同而不同。并且它还会把.pot文件自动合并到uliweb.po文件上。
Usage: uliweb i18n [options] <appname, appname, ...>
参数说明:
--apps | 对所有app进行处理。 |
-p | 处理整个项目。 |
-d DIRECTORY | 处理指定目录。 |
--uliweb | 只处理uliweb本身。 |
-l LOCALE | 如果没有指定则为en。否则按指定名字生成相应的目录。 |
如果最后给出app的列表,则会按指定的app进行处理。但一旦给出了–apps参数, 则app列表将无效。
示例:
uliweb i18n -d plugs -l zh_CN #处理plugs目录
uliweb i18n --apps -l zh_CN #全部全部app的处理
uliweb i18n -l zh_CN Test #只处理 Test app
uliweb i18n -p #整个项目,使用en
support¶
Usage: uliweb support supported_type
向当前的项目添加某种平台的支持文件。目前支持gae和doccloud。
- gae平台
- 将额外拷贝app.yaml和gae_handler.py。
- dotcloud平台
- 将额外拷贝requirements.txt和wsgi.py。不过一般情况下你有可能要修改requirements.txt 以满足你的要求。
shell¶
在当前项目目录下,进入shell环境。可以直接使用如application, settings.ini等全局 变量。
其它App包含的命令¶
orm app¶
orm app带有一系列针对数据库操作的命令,从0.1版本开始,uliorm开始支持多数据连接的
设置,具体的使用参见 ORM 的文档。同时在命令行工具上也支持对不同数据连接,它们共
同使用 --engine
参数。缺省为 default
。其它的数据库连接要在 settings.ini
中进行设置。
可用命令列举如下:
alembic¶
alembic 是用于 sqlalchemy的数据库迁移工具。目前Uliweb已经集成了alembic的部分命令,分别为:
init 初始化alembic环境
revision 生成一个版本
-m --message Message 可选的消息
--autogenerate 自动生成版本
diff 相当于使用revision时自动带autogenerate
-m --message Message 可选的消息
upgrade revision 升级指定的版本,当前版本为head
--sql 不真正执行升级,而是生成sql
--tag TAG 指定一个tag(不是太明白作用)
help <subcommand> 查看某个子命令的帮助
全部选项:
--engine Name | 引擎名称 |
和数据库的命令一样,alembic也支持加入 --engine
选项,用来选择数据库的连接。
上面所有的alembic的子命令都支持 --engine
选项。如果没有设置,缺省为 default
.
在使用时,一般先执行 init
进行初始化,如:
uliweb alembic init [--engine other]
如果是使用缺省数据库连接,则不用带其它的参数。如果要指定其它的数据库连接,则要
使用 --engine
来指定。执行后,uliweb会在当前的目录下创建形为:
project/
alembic/
<engine>/
alembic.ini
versions/
env.py
script.py.mako
alembic.ini
为alembic使用的配置文件。 env.py
为自动处理时要调用的脚本。
uliweb主要是针对这两个文件进行了定制性的处理。上面的目录结构将会为每个数据库连接
创建一个目录。这样不同的数据库的脚本将分别存放。
在使用alembic命令前,首先要安装它,简单的命令为:
pip install alembic
syncdb¶
自动根据已安装的app中settings.ini中所配置的MODELS信息,在数据库中创建不存在的表。 如果只是写在models.py中,但是未在settings.ini中进行配置,则不能自动创建。
settings.ini中的写法如:
[MODELS]
question = 'ticket.models.Question'
其中key是与Model对应的真正的表名,不能随便起。
sql¶
Usage: uliweb sql <appname, appname, ...>
用于显示对应app的Create语句。但是目前还无法显示创建Index的信息。
命令后面可以跟若干app名字,如果没有给出,则表示整个项目。
sqldot¶
Usage: uliweb sqldot <appname, appname, ...>
类似sql命令,但是它会将表及表的关系生成.dot文件,可以使用graphviz将dot文件转 为图形文件。
dump¶
Usage: uliweb dump [options] <appname, appname, ...>
将数据从数据库中卸载下来。
参数说明:
-o OUTPUT_DIR | 数据文件输出路径。缺省在项目目录的./data目录下。 |
-t, --text | 将数据以纯文本格式卸载下来。 |
--delimiter=DELIMITER | |
文本文件字段的分隔符。缺省为’,’。需要与-t连用。 | |
--encoding=ENCODING | |
文本文件字符字段所使用的编码。缺省为’utf-8’。需要与-t连用。 | |
-z zipfilename | 将导出的文本写入zipfilename中。 |
Note
dump系列函数在0.1版本后有所变化。因为支持了多数据库,所以缺省情况下,dump
出来的文件将存放到 data/default
目录下。如果指定了 --engine other
参数,则
文件将存放到 data/other
目录下。同时load系列函数也会作相同的处理。
dumptablefile¶
Usage: uliweb dumptablefile [options] tablename text_filename
将指定的表数据卸载到指定的文件中。此命令与dump和dumptable不同的地方是:这个命令 只处理一个表,并且可以指定输出文件名。而后两个命令不能指定文件名,它将按表名生 成文件名,并且放到指定的目录下。
参数说明:
-t, --text | 将数据以纯文本格式卸载下来。 |
--delimiter=DELIMITER | |
文本文件字段的分隔符。缺省为’,’。需要与-t连用。 | |
--encoding=ENCODING | |
文本文件字符字段所使用的编码。缺省为’utf-8’。需要与-t连用。 |
load¶
Usage: uliweb load [options] <appname, appname, ...>
将数据装入到数据库中。
参数说明:
-d DIR | 数据文件所存放的目录。 |
-t, --text | 将数据以纯文本格式进行处理。 |
--delimiter=DELIMITER | |
文本文件字段的分隔符。缺省为’,’。需要与-t连用。 | |
--encoding=ENCODING | |
文本文件字符字段所使用的编码。缺省为’utf-8’。需要与-t连用。 |
loadtablefile¶
Usage: uliweb loadtablefile [options] tablename text_filename
将指定的文件装入到对应的表中。
参数说明:
-t, --text | 将数据以纯文本格式进行处理。 |
--delimiter=DELIMITER | |
文本文件字段的分隔符。缺省为’,’。需要与-t连用。 | |
--encoding=ENCODING | |
文本文件字符字段所使用的编码。缺省为’utf-8’。需要与-t连用。 |
validatedb¶
Usage: uliweb validatedb [-t] <appname, appname, ...>
对指定的app或整个数据库进行结构校验,检查数据库中的字段与源码中的Model定义是否 一致。
数据库和ORM¶
Uliweb内置了一个ORM,不过它是通过orm这个app来安装的,所以缺省情况下,ORM不是自 动生效的。因此,你可以自已使用其它的ORM或数据相关的模块。当然,Uliweb的orm(以下 简称uliorm)也提供了不错的功能,欢迎使用和提改进意见。uliorm是基于sqlalchemy开 发的,并且目前没有使用session机制,而且你可以直接使用一些sqlalchemy底层的功能,如: select, update, join等。
使用要求¶
需要安装sqlalchemy 0.6+以上的版本。如果你使用sqlite,则python 2.5+就自带了。如果 使用其它的数据库,则还需要安装相应的包。sqlalchemy本身是不带的。
建议使用0.7+以上的版本。
配置¶
首先将 uliweb.contrib.orm
添加到 apps/settings.ini
的 INSTALLED_APPS
中去。
uliweb.contrib.orm
的settings.ini中已经提供了几个缺省的配置项,用来控制ORM
的行为:
[ORM]
DEBUG_LOG = False
AUTO_CREATE = True
AUTO_DOTRANSACTION = True
CONNECTION = 'sqlite:///database.db'
CONNECTION_TYPE = 'long'
CONNECTION_ARGS = {}
STRATEGY = 'threadlocal'
CONNECTIONS = {}
[MIDDLEWARES]
transaction = 'uliweb.orm.middle_transaction.TransactionMiddle'
你可以在apps/settings.ini中覆盖它们。
DEBUG_LOG
用来切换是否显示SQLAlchemy的日志。如果设置为True,则SQL语句会输出
到日志中。缺省为False。
AUTO_CREATE
用于切换是否可以自动建表。在开发的时候,最简单的情况就是当你定
义完一个Model,那么就可以直接使用它了。Uliorm会自动在数据库中创建表。如果设置为
False,则需要手工建表。要么,你可以直接手工写Create语句,然后到数据库中去创建表,
但是我们一般不会使用这种方法。要么,你可以通过uliweb sql <appname>来生成建表的
SQL语句,然后再到数据库中执行这些语句。但是这种做法,不会将Model中定义的索引也
自动创建(因为SQLAlchemy目前显示建表的SQL功能不能简单地显示索引创建的SQL代码)。
所以还不是完全的。而采用uliweb syncdb就可以自动将没有创建过的表进行创建。注意:
它只会创建没有创建过的表。对于已经创建,但是修改过的表应该如何重建呢?答案是使用
uliweb reset命令。
Note
自动建表对于sqlite有一个问题。如果在你执行一个事务时,非查询和更新类的语句 会引发事务的自动提交。而自动建表就是会先查找表是否存在,因此会破坏事务的处理。 所以建议对于sqlite禁止自动建表,而是手工建表。其它的暂时还没有发现。
AUTO_DOTRANSACTION
用于指示是否在执行 do_
时自动根据环境来创建新的共享
的线程连接并启动事务,详情参见下面关于多数据库连接的说明。
CONNECTION
用于设置数据库连接串。它是遵循SQLAlchemy的要求的。(详情可以参考- http://www.sqlalchemy.org/docs/05/dbengine.html#create-engine-url-arguments)
普通的格式为:
driver://username:password@host:port/database
示例如下:
#sqlite
sqlite_db = create_engine('sqlite:////absolute/path/to/database.txt')
sqlite_db = create_engine('sqlite:///d:/absolute/path/to/database.txt')
sqlite_db = create_engine('sqlite:///relative/path/to/database.txt')
sqlite_db = create_engine('sqlite://') # in-memory database
sqlite_db = create_engine('sqlite://:memory:') # the same
# postgresql
pg_db = create_engine('postgres://scott:tiger@localhost/mydatabase')
# mysql
mysql_db = create_engine('mysql://scott:tiger@localhost/mydatabase')
# oracle
oracle_db = create_engine('oracle://scott:tiger@127.0.0.1:1521/sidname')
# oracle via TNS name
oracle_db = create_engine('oracle://scott:tiger@tnsname')
# mssql using ODBC datasource names. PyODBC is the default driver.
mssql_db = create_engine('mssql://mydsn')
mssql_db = create_engine('mssql://scott:tiger@mydsn')
# firebird
firebird_db = create_engine('firebird://scott:tiger@localhost/sometest.gdm')
CONNECTION_TYPE
用于指明连接模式: ‘long’ 为长连接,会在启动时建立。
‘short’ 为短连接,只会在每个请求时建立。使用短连接需要配置 middle_transaction 。
使用长连接并不需要配置这个middleware。缺省值为 ‘logn’ 即长连接。
CONNECTION_ARGS
用于除连接串之外的一些参数。SQLAlchemy中,创建引擎时要使用:
create_engine(connection, **args)
而CONNECTION_ARGS将传入到args中。在某些connection中其实还可以带一些类QUREY_STRING
的东西,如在对mysql的连接中,可以在连接串后面添加 '?charset=utf8`
。而这个参
数是会直接传给更底层的mysql的驱动。而CONNECTION_ARGS是传给create_engine的,所以
还是有所不同。
STRATEGY
连接策略。此为sqlalchemy的连接参数,可以选择的值为 plain
和 threadlocal
.
其中 threadlocal
是为了实现无连接的执行。在sqlalchemy中,一般我们在执行sql命令
时或者使用engine或connection来执行。这样有时会感觉比较麻烦。于是如果在创建连接
时使用 strategy='threadlocal'
来创建,那么会在线程范围内创建一个共享的连接,
这样在执行sql时只要如:
select().execute()
就可以了。这就是无连接的执行方式。不过这样的方式在我的使用过程中感觉也有一点问题.
主要就是连接池的问题。uliweb在缺省情况下会采用长连接的策略。于是在执行完一个请求
时会close掉连接,这样可以把连接释放回去。但是发现 threadlocal 方式释放有问题,因为
它是共享的,其实无法真正的释放。所以uliweb在每个请求进来时会主动创建连接,然后在
返回时进行释放。它使用的并不是共享方式的连接。那么共享方式的连接主要是在命令行
或批处理执行时使用比较方便。在View处理中,建议都使用 do_
来进行包装。
CONNECTIONS
数据库多连接设置。uliweb是支持多个数据库连接,自然也支持多个数据库。
为了保持和以前使用方式的兼容。在 CONNECTIONS
中一般只要设置非缺省的数据库,
而缺省的数据库仍然使用原来的处理方式。 CONNECTIONS
的设置格式为:
CONNECTIONS = {
'test': {
'CONNECTION':'mysql://root:limodou@localhost/test2?charset=utf8',
'CONNECTION_TYPE':'short',
}
}
上面代码设置了一个名为 test
的连接。 CONNECTIONS
本身是一个dict,可以
设置多个连接。每个连接可以使用的参数为:
DEBUG_LOG = False
CONNECTION =
CONNECTION_TYPE = 'long'
CONNECTION_ARGS = {}
STRATEGY = 'plain'
- MIDDLEWARES
- 安装 uliweb.contrib.orm app会自动添加 TransactionMiddle ,这样将自动启动事务。 0.1.1修改
Model 定义¶
一般情况下,你应该在app下的models.py中创建Model。从uliweb.orm中导入所有东西,然
后创建自已的Model,它应该从 Model
类进行派生。然后添加你想要定义的字段。例如:
from uliweb.orm import *
import datetime
class Note(Model):
username = Field(CHAR)
message = Field(TEXT)
homepage = Field(str, max_length=128)
email = Field(str, max_length=128)
datetime = Field(datetime.datetime, auto_now_add=True)
表名¶
缺省情况下,表名应该是Model类名的小写。比如上面的Note的表名应该是 note
。
如果你想设置为其它的表名,你可以在Model类中定义一个 __tablename__
,例如:
class Note(Model):
__tableame__ = 't_note'
表别名¶
在后面我们会了解 Model 在使用时都需要配置,每个Model会有一个名字,因此我们可以
使用 get_model(name)
来获得一个Model对象。通常情况下Model的配置名和表名是
相同的(即Model的类名小写),但有时可能也需要有所不同。所以在0.1版本以后就可以
和表名不相同了。设置别名有两种方式, 一种是通过框架来使用Model,所以只要在settings
中配置就可以了。另一种是不通过框架来使用Model,如直接import,那么可以在Model类上
设置 __alias__
。
表参数¶
在SQLAlchemy中,当你创建一个表时,你可以传入一些额外的参数,例如: mysql_engin等。
所以,你可以在Model类中定义 __table_args__
,例如:
class Todo(Model):
__table_args__ = dict(mysql_charset='utf8')
连接引擎设置¶
uliweb支持多种数据库连接的设置,其中可以在Model中设置 __engine_name__
为指定
的某个连接名,如:
class Todo(Model):
__engine_name__ = 'test'
OnInit 方法¶
uliorm也允许你在创建表之时在一些初始化工作。只要写一个OnInit的class method,例 如:
class Todo(Model):
@classmethod
def OnInit(cls):
Index('my_indx', cls.c.title, cls.c.owner, unique=True)
上面的代码是用来创建复合索引。一般的单字段索引,可以在定义字段时直接指定Index=True。
default_query 方法¶
uliorm目前支持用户自定义缺省条件,即在查询时,会自动将缺省条件与输入的条件合并 处理,它需要定义为一个类方法,如:
class Todo(model):
@classmethod
def default_query(cls, query):
return query.filter(xxx).order_by(yyy)
default_query 将传入一个query对象,你可以对它使用Result上的查询相关的处理,比如:
filter
, order_by
, limit
, offset
等可以返回结果集的方法。
属性定义¶
uliorm中定义一个Model的字段为Property,但为了方便,uliorm还提供了Field函数。
所有的字段都是以Property结尾的类。下面是uliorm中的字段类:
'BlobProperty', 'BooleanProperty', 'DateProperty', 'DateTimeProperty',
'TimeProperty', 'DecimalProperty', 'FloatProperty',
'IntegerProperty', 'Property', 'StringProperty', 'CharProperty',
'TextProperty', 'UnicodeProperty', 'FileProperty', 'PickleProperty'
你可能认为它们不好记忆,所以你可以使用Field来定义。
Field是一个函数,它的第一个参数可以是内置的Python type,也可以是uliorm定义的特殊 类型。其它的参数是和对应的Property类一致的。它会根据你传入的Python type或特殊类 型来自动查找匹配的字段类。
Python type和字段类的对应关系为:
引用简写类型 | 实际类型 |
---|---|
str | StringProperty, |
CHAR | CharProperty, |
unicode | UnicodeProperty, |
TEXT | TextProperty, |
BLOB | BlobProperty, |
FILE | FileProperty |
int | IntegerProperty, |
float | FloatProperty, |
bool | BooleanProperty, |
datetime.datetime | DateTimeProperty, |
datetime.date | DateProperty, |
datetime.time | TimeProperty, |
decimal.Decimal | DecimalProperty, |
DECIMAL | DecimalProperty, |
PICKLE | PickleProperty, |
小写的,都是Python内置的类型或类。大写的都是uliorm为了方便记忆而创建的。而上面 看到的关于Node的示例就是使用Field来定义字段的。
ID 属性¶
缺省情况下,uliorm会自动为你添加一个 id
字段,而你并不需要在Model中进行定义。
Property 构造函数¶
Property 其它所有字段类的基类。所以它的一些属性和方法将会被派生类使用到,它的定 义为:
Property(verbose_name=None, name=None, default=None, required=False,
validators=None, choices=None, max_length=None, type_class=None,
type_attrs=None)
- verbose_name
- 用于显示字段的描述信息。一般是用在显示界面上。
- name
字段名,用在所创建的表中。它一般是和Property的实例名相同。例如:
class User(Model): username = StringProperty(name='user_name')
username就是Property的实例名,而name缺省不给出的话就是
username
, 上面的 示例是指定了一个不同的值。因此你通过orm引用属性时要使用username
,但是 直接对数据库查询或操作时,即要使用user_name
, 因此为了避免造成理解和使用 上的混乱,建议不要指定name
参数。- default
- 字段的缺省值。注意,default可以是一个函数。在创建一个Model的实例时,对于未 给出值的属性,uliorm会自动使用default给字段赋值。因此,如果default没有赋值, 则这个值一般为None。但是对于象IntegerProperty之类的特殊字段来说,缺省值不是None,如 0。同时,在调用时要注意default函数执行是否可以成功。因为有的时候需要 在某个环境下,而你在执行时可能不具备所要求的环境,比如default函数要处理request.user, 但是你有可能在批处理中去创建实例,这样request.user是不会存在的,因此会报错。 简单的处理就是把Model.field.default置为None。
- required
- 指明字段值是否不能为None。如果在创建Model实例时,没有传入required的字段值, 则uliorm会检查出错。同时这个属性可以用在Form的处理中。
- validators
当给一个属性赋值时,uliorm会根据这个参数来校验传入值的合法性。它应该是一个 函数,这个函数应写为:
def validator(data): xxx if error: raise BadValueError, message
如果校验失败,这个函数应该抛出一个 BadValueError的异常。如果成功,则返回 None或不返回。
- choices
- 当属性值的取值范围是有限时可以使用。它是一个list,每个元素是一个二元tuple, 格式为(value, display),value为取值,display为显示信息。目前,uliorm并不用 它来校验传入数据的正确性,用户可以根据需要自定义校验函数,传入validators中 进行校验处理。
- max_length
- 字段的最大长度,仅用在
StringProperty
,CharProperty
中。如果没 有指定缺省为30。 - index
- 如果设置为True则表示要使用当前字段生成索引。只适合单字段索引。如果要生成复 合索引,要生成OnInit类方法,并调用Index函数来生成。缺省为False。
- unique
- 表示字段是否可以重复。缺省为False。
- nullable
- 指示在数据库中,本字段是否可以为
NULL
。缺省为True。 - type_class, type_attrs
- 可以用来设置指定的SQLAlchemy的字段类型并设置要传入的字段属性。如果有长度值, 则是在max_length中指定。
字段列表¶
CharProperty¶
与 CHAR
相对应。你应该传入一个 max_length
。如果传入一个Unicode字符串它
将转换为缺省编码(utf-8)。
StringProperty¶
与 VARCHAR
相对应。你应该传入一个 max_length
。如果传入一个Unicode字符串它
将转换为缺省编码(utf-8)。目前uliorm从数据库中取出StringProperty时会使用Unicode,
而不转换为utf-8或其它的编码。因此与UnicodeProperty是一致的。
TextProperty¶
与 TEXT
相对应。用于录入大段的文本。
UnicodeProperty¶
与 VARCHAR
相对应。但是你需要传入Unicode字符串。
BlobProperty¶
与 BLOB
相对应。用于保存二进制的文本。
DateProperty DateTimeProperty TimeProperty¶
这些字段类型用在日期和时间类型上。它们还有其它的参数:
- auto_now
- 当设置为True时,在保存对象时,会自动使用当前系统时间来更新字段的取值。
- auto_add_now
- 当设置为True时,仅创建对象时,会自动使用当前系统时间来更新字段的取值。
- format
用来设置日期时间的格式串,uliorm会用它进行日期格式的转换。在缺省情况 下,当传入一个字符串格式的日期字段时,uliorm会进行以下尝试:
格式串 样例 ‘%Y-%m-%d %H:%M:%S’ ‘2006-10-25 14:30:59’ ‘%Y-%m-%d %H:%M’ ‘2006-10-25 14:30’ ‘%Y-%m-%d’ ‘2006-10-25’ ‘%Y/%m/%d %H:%M:%S’ ‘2006/10/25 14:30:59’ ‘%Y/%m/%d %H:%M’ ‘2006/10/25 14:30’ ‘%Y/%m/%d ‘ ‘2006/10/25 ‘ ‘%m/%d/%Y %H:%M:%S’ ‘10/25/2006 14:30:59’ ‘%m/%d/%Y %H:%M’ ‘10/25/2006 14:30’ ‘%m/%d/%Y’ ‘10/25/2006’ ‘%m/%d/%y %H:%M:%S’ ‘10/25/06 14:30:59’ ‘%m/%d/%y %H:%M’ ‘10/25/06 14:30’ ‘%m/%d/%y’ ‘10/25/06’ ‘%H:%M:%S’ ‘14:30:59’ ‘%H:%M’ ‘14:30’
BooleanProperty¶
与 Boolean
相对应。不过对于不同的数据库底层可能还是不同。具体是由SQLAlchemy
来实现的。
IntegerProperty¶
与 Integer
对应。
FileProperty¶
与 VARCHAR
对应。用于保存文件名,而不是文件对象。缺省的max_length为255。
PickleProperty¶
有时我们需要将一个Python对象保存到数据库中,因此我们可以采用 BLOB
字段来处理。
首先将对象序列化为字符串,可以使用Python自带的pickle,然后写入数据库。读出时再
反序列化为Python的对象。使用 PickleProperty
可以把这一过程自动化。
Model的常见属性¶
- table
- uliorm的Model对应于SQLAlchemy的
Table
对象,而table
将是底层的 Table的实例。所以你可以使用这个属性来执行表级的操作。 - c
- Model的字段集。与 table.c 属性是一样的。
- properties
- 所有定义在Model中的属性。
- metadata
- 与SQLAlchemy中的metadata相对应的实例。
- tablename
- 表名。
Note
Uliweb中Model对应的表名一方面可以通过 __tablename__
来指定。另一方面,它
可以将Model的类名小写作为表名。
关系定义¶
uliorm支持以下几种关系的定义: OneToOne, Reference, SelfReference, ManyToMany.
OneToOne¶
OneToOne是用来定义一对一的关系。
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = OneToOne(Test)
... name = Field(str)
可以使用OneToOne的关系来直接引用另一个对象。例如:
>>> a1 = Test(username='limodou')
>>> a1.save()
True
>>> b1 = Test1(name='user', test=a1)
>>> b1.save()
True
>>> a1
<Test {'username':'limodou','year':0,'id':1}>
>>> a1.test1
<Test1 {'test':<Test {'username':'limodou','year':0,'id':1}>,'name':'user','id':1}>
>>> b1.test
<Test {'username':'limodou','year':0,'id':1}>
在定义OneToOne时,可以传入一个collection_name的参数,这样,可以用这个名字来反向 引用对象。如果没有给出collection_name,则将使用表名作为引用名。
Note
注意,OneToOne只是一个关系,它并不会自动根据主表记录自动创建关联表的记录。
Reference¶
uliorm使用 Reference
来定义多对一的关系。
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = Reference(Test, collection_name='tttt')
... name = Field(str)
>>> a1 = Test(username='limodou1')
>>> a1.save()
True
>>> b1 = Test1(name='user', test=a1)
>>> b1.save()
True
>>> b2 = Test1(name='aaaa', test=a1)
>>> b2.save()
True
>>> a1
<Test {'username':'limodou1','year':0,'id':1}>
>>> list(a1.tttt.all())[0] #here we use tttt but not test1_set
<Test1 {'test':<Test {'username':'limodou1','year':0,'id':1}>,'name':'user','id':1}>
>>> a1.tttt.count()
2
上面的例子演示了多个Test1记录可能对应一个Test记录。因此,我们可以在Test1中
定义 Reference
到Test上。对于Test1的某个实例,假定为b1,我们就可以通过
b1.test来获得对应的Test对象。这里会自动引发一个查询。如果你想从Test的某个对
象来反向获取Test1应该怎么办呢?假定Test的对象实例为a1,则缺省情况下我们可以通
过a1.test1_set.all()来获得a所对应的所有Test1的实例。为什么是all()呢?因为一个
Test对象有可能对应多个Test1对象(这就是多对一关系),所以得到的可能不仅一条
记录,应该是一个结果集。再看一下 test1_set
,它就是Test1的表名加 _set
后缀。但是,如果Test1中有多个字段都是到Test的Reference会出现什么情况。这时,
Uliweb会抛出异常。原因是,这样会在Test类中出现多个同名的test1_set属性,这是
有冲突的。所以当存在多个到同一个表的引用时,要进行改名。而Reference提供了一个
collection_name
的参数,可以用它来定义新的别名。比如上面的 tttt
。这样
在获取a1所对应的Test1的记录时,就可以使用 a1.tttt
来反向获取了。
Refernce有以下几个参数可以使用:
- reference_class
- 第一个参数,指明要关联的Model。可以是Model类,也可以是字符串形式的表名。 如果是第二种用法,则要与get_model配合使用。详见get_model的用法说明。
- collection_name
- 前面已经介绍,是反向获取记录的名字
- verbose_name
- 字段的提示信息
- reference_fieldname
- 当引用一个Model时,缺省情况下是使用该Model的id字段。但是在特殊情况下,你可
能希望指定其它的字段。这样可以将要引用的字段名传给
reference_fieldname
参数。这样uliorm会根据被引用的字段来动态创建字段的类型。 - required
- 是否是必输项。缺省为False。
Note
uliorm的Reference关系并不会生成ForeignKey的外键。因为,一旦使用外键,则删除 导入数据时都有一个执行顺序,非常难处理。所以在设计上没有采用外键。
SelfReference¶
如果你想引用自身,你可以使用 SelfReference
, 例如:
>>> class User(Model):
... username = Field(unicode)
... parent = SelfReference(collection_name='children')
ManyToMany¶
>>> class User(Model):
... username = Field(CHAR, max_length=20)
... year = Field(int)
>>> class Group(Model):
... name = Field(str, max_length=20)
... users = ManyToMany(User)
>>> a = User(username='limodou', year=5)
>>> a.save()
True
>>> b = User(username='user', year=10)
>>> b.save()
True
>>> c = User(username='abc', year=20)
>>> c.save()
True
>>> g1 = Group(name='python')
>>> g1.save()
True
>>> g2 = Group(name='perl')
>>> g2.save()
True
>>> g3 = Group(name='java')
>>> g3.save()
True
>>> g1.users.add(a)
>>> g1.users.add(b)
你可以使用 ManyToMany
来指明一个多对多的关系. uliorm会象Django一样自动创建
第三张表,上例的第三张表会是: group_user_usres
, 它是由两个表名(user和group)
和关系名(users)组成. 第三张表的表结构会是:
CREATE TABLE group_user_users (
group_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
PRIMARY KEY (group_id, user_id)
)
操作¶
ORM的操作可以分为不同的级别: 实例级、Model级和关系级。
- 实例级
- 这类操作只会影响实例自身,你可以进行: 创建、获取、删除、更新等操作。
- Model级
- 这类操作所处理的范围是整个Model或表级,它主要进行集合性质的操作。你可以进行: 查询、计数、排序、删除、分组等操作。
- 关系级
- 不同的关系可以执行不同的操作。如:OneToOne可以进行实例级操作。而Reference, SelfReference和ManyToMany则可以进行集合操作。在使用关系时,一种我们是使用 inst.relationship的方式,这样会自动将关系与正在处理的实例进行条件的绑定, 另一种是通过Model.relationship的方式,这样可以调用关系字段的某些特殊方法, 比如用来生成条件。
实例级¶
创建实例¶
假定有一个 User Model,类的定义为:
class User(Model):
username = Field(CHAR, max_length=20)
year = Field(int)
所以,如果你想要创建一个User的实例,只要:
user = User(username='limodou', year=36)
但这样还不会保存到数据库中,它只是创建了一个实例,你还需要调用 save
来保存:
user.save()
获取实例¶
user = User.get(5)
user = User.get(User.c.id==5)
可以通过Model.get()来获取一个实例。在get()中是条件。如果是一个整数,则认为是要
获取id等于这个值的记录。否则你可以使用一个条件。这里条件的写法完全是遵守 SQLAlchemy
的要求。如果条件不止一个,可以使用 and_, or_, not_
或 &, |, ~
来拼接条件。SQLAlchemy
的相关文档可以查看: http://www.sqlalchemy.org/docs/core/tutorial.html
Note
注意,在结果集上,你可以多个使用filter()连接多个 and
的条件,而get不支
持这样的用法。比如你可以 User.filter(User.c.id=5).filter(User.c.year>30)。
user = User.get_or_notfound(5)
使用get_or_notfound可以当无满足条件的对象时抛出一个NotFound的异常。
删除实例¶
user = User.get(5)
user.delete()
delete在删除对象时,会自动删除相关联的ManyToMany的关系数据。如果不想删除,则可以
传入 manytomany=False
。
更新实例¶
user = User.get(5)
user.username = 'user'
user.save()
更新实例可以直接向实例的某个字段赋予新值,也可以使用update方法来一次更新多个字 段。如:
user.update(username='user')
user.save()
Note
注意,象创建和更新时,在调用相关的方法时,你传入的是key=value的写法,这里 key就是字段的名字。但是在写条件时,你要使用 Model.c.fieldname 这样的写法, 并且不是赋值,而是python的各种运算符。不要搞错了。
Uliorm在保存时会根据对象的id值是否为None来判断是否是insert还是update。如果你直接
设置了id值,但是又希望通过insert来插入数据,可以在调用save时传入 insert=True
。
Attention
Model中更新数据库相关的方法,如: save, delete, get, get_or_notfound, count, remove 都可以传入connection参数,它可以是数据库连接名或真正的连接对象。
其它的API¶
- to_dict(fields=[], convert=True, manytomany=False)
将实例的值转为一个dict对象。如果没有给出fields参数,则所有字段都将转出。 注意,这里对
ManyToMany
属性有特殊的处理。因为ManyToMany
属性并 不是真正的表中的字段,所以缺省情况下是不会包含这些值的,如果指定manytomany为 True,则会也把相应的ManyToMany
所对应的对象集的ID取出来,组织为一个list。 如果convert=True,则在取出字段值的时候,还会调用field_str函数进行值的处理。 在调用field_str时,strict保持为False不变。举例:
a = User.get(1) a.to_dict() #this will dump all fields a.to_dict(['name', 'age']) #this will only dump 'name' and 'age' fields
- field_str(v, strict=False)
- 将某个字段的值转为字符串表示。如果strict为False,则只会处理日期类型、Decimal 类型和将Unicode转为字符串。如果strict为True,则:None会转为’‘,其它的全部转为 字符串。
- get_display_value(field_name)
- 返回指定字段的显示值。特别是对于包含有choices的字段,可以根据相应的值返回对 应的choices的值。
- get_datastore_value(field_name)
返回指定字段的数据库的值。特别是对于
Reference
字段,如果直接使用inst.reference 则得到的会是引用的对象,而不是数据库保存的值。而使用get_datastore_value()
可以得到数据库的值。Note
uliorm会将
Reference
字段保存到_field_name_
的属性中,因此可以 直接使用它来得到Reference
的值。比如User.c.system
可能是指向System
表的引用,直接使用user.system
会得到对象的System
的对象。而使用user._system_
则得到对应的数据库的值。
Model级¶
uliorm在Model级上的操作主要有两类,一类是直接通过Model.func来调用的,另一类是通 过Model.func或Model.relationship的方式返回结果集,再在结果集上进行操作。对于与 查询相关的函数,是可以连在一起使用的,比如:
User.filter(...).filter(...).count()
有些方法会返回结果集,因此你可以在返回值的基础上,再调用查询相关的方法。有些方法会 直接返回结果,不能再调用查询相关的方法。
查询¶
在查询一个表的时候可能会有两种需求:全部记录和按条件筛选,因此对应着可以使用
all()
和 filter()
。all()
中是没有参数的,它会返回一个 Result
对象,这是前面介绍的结果集,你可以在结果集上继续使用其它的方法。 filter()
需要传入条件,条件的写法是符合SQLAlchemy要求的。它也返回一个结果集。多个 filter()
是可以连接使用的,相当于多个与条件。
举例:
User.all()
User.filter(User.c.year > 18)
删除记录¶
Model中提供了 remove(condition)
来删除满足条件的记录。同时你也可以利用结果
集来删除。例如:
User.remove(User.c.year<18)
#等价于
User.filter(User.c.year<18).remove()
Note
注意,结果集的删除是使用 remove
,而实例的删除是使用 delete
。
记录条数统计¶
Model中提供了 count(condition)
来计算满足条件的记录数。同时你也可以利用结果
集来统计,例如:
User.count(User.c.year<18)
#等价于
User.filter(User.c.year<18).count()
其它 API¶
- bind(metadata=None, auto_create=False)
- 绑定当前的类到一个metadata对象上。如果
auto_create
为True
, 则将 自动建表。 - create()
- 建表,并且会自动检查表是否存在。
- connect()
- 切換数据库连接,这样后续的执行将在新的数据库连接上进行。
- get_engine_name()
获得当前表所使用的数据库连接的名字。在多个地方都可以设置数据库连接,uliweb 将按以下顺序来判断:
- 是否设置了
__engine_name__
- 是否在
settings.ini
中设置了对应的连接名 'default'
这样在缺省情况下,数据库连接名为
default
.- 是否设置了
关系级¶
一对一(One to One)¶
一对一关系没什么特别的,例如:
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = OneToOne(Test)
... name = Field(str)
>>> a = Test(username='limodou', year=36).save()
>>> b = Test1(name='user', test=a).save()
>>> b.test
<Test {'username':'limodou', 'year':36}>
所以你可以使用 b.test
如同 a
对象。
Note
注意,关系的建立是在相关的对象创建之后,而不是会根据关系自动创建对应的对象。
多对一(Many to One)¶
>>> class Test(Model):
... username = Field(str)
... year = Field(int)
>>> class Test1(Model):
... test = Reference(Test, collection_name='tttt')
... name = Field(str)
>>> a = Test(username='limodou').save()
>>> b = Test1(name='user', test=a).save()
>>> c = Test1(name='aaaa', test=a).save()
根据上面的代码, Test:Test1 是一个 1:n 关系。并且 b.test
是对象 a
。但是
a.tttt
将是反向的结果集,它可能不止一个对象。所以 a.tttt
将返回一个 Result
对象。并且这个结果集对象将绑定到 Test1 Model,所以结果集的 all()
和 filter()
方法将只返回 Test1 对象。更多的细节可以查看 Result
的描述。
多对多(Many to Many)¶
>>> class User(Model):
... username = Field(CHAR, max_length=20)
... year = Field(int)
>>> class Group(Model):
... name = Field(str, max_length=20)
... users = ManyToMany(User)
>>> a = User(username='limodou', year=5).save()
>>> b = User(username='user', year=10).save()
>>> c = User(username='abc', year=20).save()
>>> g1 = Group(name='python').save()
>>> g2 = Group(name='perl').save()
>>> g3 = Group(name='java').save()
>>> g1.users.add(a)
>>> g1.users.add(b)
当你调用 a.group_set
(因为你没有在ManyToMany属性中定义collection_name)或
g1.users
时,将返回一个 ManyResult
对象。
Result 对象¶
Result
对象的生成有多种方式,一种是执行某个关系查询时生成的,一种是直接在
Model上调用 all()
或 filter()
生成的。Result
对象有多个方法可以调
用,有些方法,如 filter()
会返回 Result
本身,因此还可以继续调用相应的
方法。有些方法直接返回结果,如: one()
, count()
。因此你可以根据不同的
方法来考虑是不是使用方法的连用形式。
注意, Result
对象在调用相应的方法时,如果返回的是结果集本身,此时不会立即
进行数据库的交互,而是当你调用返回非结果集的函数,或要真正获得记录时才会与数据
库进行交互。比如执行 User.filter(...).count()
时,在执行到User.filter(...)
并没有与数据库进行交互,但在执行到 count() 时,则生成相应的SQL语句与数据库进行
交互。又如:
query = User.all()
for row in query:
在执行 query = User.all()
时,并不会引发数据库操作,而在执行 for
语句时
才会真正引发数据库的操作。
同时, Result
在获取数据时,除了 one()
和 values_one()
会直接返回
一条记录或 None。all()
, filter()
, values()
会返回一个 generator。
所以如果你想要一个list对象,需要使用 list(result) 来转成 list 结果。
方法说明:
- all(): Result
- 返回Result本身. 注意在 Model中也有一个all()方法,它就是创建一个
Result
对象,然后将其返回。如果不带任何条件创建一个结果集,则在处理记录时相当 于all()的调用。 - filter(condition): Result
按条件查询。可以多个filter连用。返回结果集本身。
示例:
User.filter(User.c.age > 30).filter(User.c.username.like('Lee' + '%%'))
- connect(engine_name): Result
- 切換到指定的连接名上,engine_name可以是连接名,Engine对象或Connection对象。
- count(): int
返回满足条件的记录条数。需要与前面的all(), filter()连用。
Note
在Model中也有一个count()方法,但是它是可以带条件的,比如:
User.count(User.c.age > 30)
。 它可以等同于User.filter(User.c.age > 30).count()
示例:
User.all().count() User.filter(User.c.username == 'a').count()
- remove(): None
- 删除所有满足条件的记录。它其实是调用 Model.remove(condition)。可以和
all()
和filter()
连用。 - update(**kwargs):
执行一条update语句。例如:
User.filter(User.c.id==1).update(username='test')
它等同于:
do_(User.table.update().where(User.c.id==1).values(username='test'))
- order_by(*field): Result
向查询中添加
ORDER BY
字句。例如:result.order_by(User.c.year.desc()).order_by(User.c.username.asc()) #or result.order_by(User.c.year.desc(), User.c.username.asc())
缺省情况下是按升序排列,所以asc()可以不加。
- limit(n): Result
- 向查询中添加
LIMIT
子句。n
是一个整数。 - offset(n): Result
- 向查询中添加
OFFSET
子句。n
是一个整数。 - distinct(*field): Result
- 向查询中添加
DISTINCT
函数,field是字段列表。 - values(*fields): 结果 generator
它将根据前面设置的条件立即返回一个结果的generator。每行只会列出指定的字段值。 fields为字段列表,可以直接是字段的名字,也可以是Model.c.fieldname的形式。 例如:
>>> print a1.tttt.all().values(Test1.c.name, Test1.c.year) [(u'user', 5), (u'aaaa', 10)] >>> print a1.tttt.all().values('name', 'year') a1.tttt.all().values(Test1.c.name, Test1.c.year)
- one(): value
- 只返回结果集中的第一条记录。如果没有记录,则返回
None
。 - values_one(*fields): value
- 相当于执行了
values()
, 但是只会返回第一条记录。 - get(condition): value
- 相当于
Result.filter(condition).one()
。 - without(flag=’default_query’)
- 去掉default_query的条件处理。
ManyResult¶
ManyResult
非常象 Result
, 只不过它是通过 ManyToMany
关系创建的,它
拥有与 Result
大部分相同的方法,但是有一些差别:
- add(*objects): boolean
- 这个方法可以建立多个对象与当前对象的多对多关系。其实就是向第三张关系表中插入 相应的记录。它会返回一个boolean值。如果为 Ture 表示有变化。否则无变化。如果 Model A的实例a已经和Model B的某些实例有多对多的关系,那么当你添加新的关系时 对于已经存在的关系将不会再添加,只添加不存在的关系。
- update(*objects): boolean
- 这个方法与add()有所不同。add会在原来的基础之上添加新的关系。而update会完全 按照传入的对象来重新修改关系,对于仍然存在的关系将保留,对于不存在的关系将 删除。它也会返回是否存在修改的状态。
- ids(): list
- 它将返回ManyToMany关系中所有记录的 ID 列表。注意,这里的ID是与定义ManyToMany 属性时所使用的引用字段一致的。缺省情况下是id字段,如果使用了其它的引用字段 则有可能是别的字段。
- has(*objects): boolean
- 判断传入的对象是否存在于关系中。这里对象可以是对象的id值,也可以是对象。如果 存在则返回 True,如果不存在则返回 False。
事务处理¶
uliorm提供两种控制事务的方式,一种是通过Middleware,一种是手工处理。如果要使用 Middleware方式,你需要在settings.ini中添加:
MIDDLEWARE_CLASSES = [
'uliweb.orm.middle_transaction.TransactionMiddle'
]
使用Mideleware,它将在每个view处理时生效。当view成功处理,没有异常时,事务会被 自动提交。当view处理失败,抛出异常时,事务会被回滚。
Note
一般情况下,只有事务处理Middleware捕获到了异常时,才会自动对事务进行回滚。 因此,如果你自行捕获了异常并进行了处理,一般要自行去处理异常。
手工处理事务,uliorm提供了基于线程模式的连接处理。uliorm提供了:Begin(), Commit(), 和Rollback()函数。当执行Begin()时,它会先检查是否当前线程已经存在一个连接, 如果存在,则直接使用,如果不存在则,如果传入了create=True,则自动创建一个连接, 并绑到当前的线程中。如果create=False,则使用engine的连接。同时Commit()和Rollback() 都会使用类似的方式,以保证与Begin()中获得的连接一致。
Web事务模式¶
一般你要使用事务中间件,它的处理代码很简单,为:
class TransactionMiddle(Middleware):
ORDER = 80
def __init__(self, application, settings):
self.db = None
self.settings = settings
def process_request(self, request):
Begin()
def process_response(self, request, response):
try:
return response
finally:
CommitAll(close=True)
if self.settings.ORM.CONNECTION_TYPE == 'short':
db = get_connection()
db.dispose()
def process_exception(self, request, exception):
RollbackAll(close=True)
if self.settings.ORM.CONNECTION_TYPE == 'short':
db = get_connection()
db.dispose()
当请求进来时,执行 Begin() 以创建线程级别的连接对象。这样,如果在你的 View中要手工处理事务,执行Begin()会自动使用当前线程的连接对象。
应答成功时,执行 CommitAll(close=True)
,完成提交并关闭连接。因为有可能存在
多个连接,所以使用CommitAll. 而在View中手动控制一般只要调用 Commit()
就可以了,
关闭连接交由中间件完成。
如果中间处理抛出异常,则执行 RollbackAll(close=True)
,回滚当前事务,并关闭
所有连接。而在View中手动控制,也只要简单调用 Rollback()
就可以了,关闭连接处理由
中间件完成。
在View中的处理,有几点要注意,Begin(), Commit(), Rollback() 都不带参数调用。
在Uliorm中,SQL的执行分两种,一种是直接使用ORM的API处理,还有一种是使用SQLAlchemy
的API进行处理(即非ORM的SQL)。为了保证正确使用线程的连接对象,ORM的API已经都使用
do_()
进行了处理。 do_()
可以保证执行的SQL语句在当前的合理的连接上执行。几种
常见的SQL的书写样板:
#插入
do_(User.table.insert().values(username='limodou'))
#更新
do_(User.table.update().where(User.c.username=='limodou').values(flag=True))
#删除
do_(User.table.delete().where(User.c.username=='limodou'))
#查询
do_(select(User.c, User.c.username=='limodou'))
命令行事务模式¶
所谓命令行事务模式一般就是在命令行下运行,比如批处理。它们一般不存在多线程的环境, 所以一个程序就是一个进程,使用一个连接就可以了。这时我们可以还使用engine的连接 对象。使用时,只要简单的不带参数调用Begin(), Commit()和Rollback()就可以了。因为 Begin()在没有参数调用的情况下,会自动先判断有没有线程级的连接对象,这时一定是没有, 如果没有,则使用engine下的连接对象。
这样,SQL语句既可以使用do_()来运行,也可以使用原来的SQLAlchemy的执行方式,如:
#插入
User.table.insert().values(username='limodou').execute()
#更新
User.table.update().where(User.c.username=='limodou').values(flag=True).execute()
#删除
User.table.delete().where(User.c.username=='limodou').execute()
#查询
select(User.c, User.c.username=='limodou').execute()
NotFound异常¶
当你使用get_or_notfound()或在使用instance.refernce_field时,如果对象没找到则会 抛出NotFound异常。
Model配置化¶
uliorm在考虑Model的可替换性时,提供了一种配置机制。这种机制主要是由orm app来初 始化的,它对Model的编写有一定的要求。使用配置机制的好处主要有两点:
可以方便使用,不用关心要使用的Model是在哪里定义的。orm提供了
get_model()
方法,可以传入字符串的表名或真正的Model对象。因此在一般情况下,使用字符串 形式是最方便的。比如我们想获得一个User的Model,可以使用:User = get_model('user')
但是使用这种字符串的形式,对于Model的配置有要求。需要在settings.ini中配置:
[MODELS] user = 'uliweb.contrib.auth.models.User'
其中key为引用的别名。它可以是表名(一般为Model类名小写),也可以不是表名。 value为表所对应的Model类的路径。uliorm将在需要时自动进行导入。
Note
为什么需要表名呢?因为orm提供的命令行工具中,syncdb会自动创建数据库中 不存在的表,它就是使用的真正的表名。
Note
在使用多数据库连接时,可以在上面的MODELS中的每张表的路径后面添加数据库 连接名,如:
[MODELS] user = 'uliweb.contrib.auth.models.User', 'test' user = 'uliweb.contrib.auth.models.User', ['default', 'test']
第一种是说只在
test
中使用User表。而第二种则表示可以在default
或test
中使用User表,决定的顺序一是根据 Model 的__engine_name
的设置或执行时使用connect(engine_name)
进行设定。否则将使用第一个。可以有条件的方便进行替换。
在某些时候,你可能发现某个app的表结构要扩展几个字段,但是因为已经有许多Model 和这个表实现了关联,而且这个app提供了其它与些Model相关的一些方法。因此,如果 简单地替换这个app,有可能会要同时修改其它的app的代码,比如导入处理等。如是你 在定义关系时使用的是get_model(name)的形式,并且name是字符串,这样你实际上已经 实现了Model的配置化。因此你就可以定义新的Model类,并且配置到settings.ini中来 替换原来的Model。如果不是把配置信息写到同一个settings.ini中,那么,你可以把 新的App定义到原来的App之后(这里指INSTALLED_APPS),这样后面定义的内容会覆盖前 面定义的内容。这种做比较适合扩展字段的情况,或表结构的修改不影响其它的功能调 用的情况。
在定义关系时,象OneToOne, Reference和ManyToMany时既可以接受字符串的Model名,也 可以直接传入Model的类,都可以。
如何在其它项目中使用 uliorm¶
uliorm是可以在非Uliweb项目和非web程序中使用的,因此根据是否有Uliweb项目,决定了 可以使用不同的方式。
非Uliweb项目¶
Uliweb项目中,所有的Model都要配置到settings.ini中去,所以在非Uliweb项目中,你无 法这样做,因此处理上会有所不同。因为没有了Model的配置,所以你需要在使用Model前 先导入它们。然后你要考虑是自动建表还是手工建表。我建议是把自动建表单独处理,只 在需要时执行。简单的一个代码示例:
from uliweb.orm import *
class User(Model):
name = Field(unicode)
class Group(Model):
name = Field(str)
users = ManyToMany(User, collection_name = 'groups')
if __name__ == '__main__':
db = get_connection('sqlite://')
db.metadata.drop_all()
db.metadata.create_all()
u1 = User(name='limodou')
u1.save()
g1 = Group(name='python')
g1.save()
g1.users.add(u1)
print g1.users.one().groups.one().users.one().name
print u1.groups.one().users.one().groups.one().name
这里 db.metadata.create_all()
用于创建所有的表。
Uliweb项目¶
如果我们要在非web程序中使用uliorm时,我们还是希望使用Uliweb的管理机制,使用Uliweb 项目的配置信息,这时我们可以:
from uliweb.manage import make_simple_application
app = make_simple_application(project_dir='.')
Begin()
try:
User = get_model('user')
print list(User.all())
Commit()
except:
Rollback()
在守护中使用Uliorm的注意事务¶
其实在守护中使用uliorm就是要注意使用事务。在我自已的开发中发现一个奇怪的问题:
例如有一个循环,它的工作就是扫描数据库满足某个条件的数据集,如果有,则取出进行
处理,然后修改处理标志。处理完毕或不存在这样的数据,则sleep一定的时间。然后反复
执行。那么数据库的更新可能会时发生。原来我在循环外创建一个数据库连接,这样可以
复用这个连接。但是发现:如果开始没有数据,则后面更新了数据库也看不到数据。重启
后第一次可以找到要处理的数据。但是在等了一会再读取时,即使数据库中有数据也读
不出来。检查了半天也不知道为什么。后来把连接放到了循环中,结果一切正常。因此建议
在进行数据库处理时使用事务,并且要放在处理之前。另一种办法是使用 Connect
,它
会清理以前缓存的连接。这个问题是比较头痛,所以要么使用事务,要么执行 Connect
会比较正常。对于只执行一次的定时程序应该不存在这个问题。
模块级 API¶
uliweb.orm 提供了一些模块级别的方法,用于控制整个uliorm的工作模式。不过,如果 你不是在脱离uliweb的框架环境下来使用orm模块的话,以下的一些方法在settings.ini 中有相应的配置,因此不需要去手工调用相应的函数。但如果是在其它的非uliweb的环境 下使用uliorm,则有可能需要手工调用这些函数来控制uliorm的行为。
- set_auto_create(flag)
设置是否自动建表。flag取值为True或False。缺省为False。这一功能在开发时比较 有用,因为可以不使用uliweb syncdb来建表,但是在生产环境中建议关闭,手动来 处理。
Note
在使用sqlite时,发现有问题。当处于一个事务中,如果出现非select, update 之类的语句,sqlite会自动提交事务,造成事务处理不是按你的预期,所以也需 要关闭这个功能。
- set_debug_query(flag)
- 设置调试模式。如果flag为True,则生成的SQL语句将输出到日志中。如果你是通过
get_connection()
得到的一个数据库连接对象,可以简单地设置db.echo = True
来激活调试模式。 - set_encoding(encoding)
- 设置缺省编码。缺省为
utf-8
。 - get_connection(connection=’‘, default=True, debug=None, engine_name=None, connection_type=’long’, **args)
建立一个数据库连接,并返回连接对象。 connection需要按SQLAlchemy的要求来编写。 get_connection既可以支持原来的单数据库连接模式,也可以支持多数据库连接模式, 还可以支持缺省连接模式,既上次创建过,然后复用原来的连接。那么它按以下策略 来处理:
if connection 不为空: 则缺建新的连接 if default is True: 则将连接设置到线程中进行共享 else: 不共享 else: 按engine_name来返回连接。如果engine_name为None,则使用 default
- get_model(model, engine_name=None)
返回指定连接的
model
对应的Class。如果是字符串值,则需要根据Model配置的要求在settings.ini 中定义Model的信息才有效果。也可以传入Model的类。如果engine_name不为None,则根据给定的engine_name来查找Model。如果不存在,则 抛出异常。
如果engine_name为空时,将会智能搜索。如果某个Model只设置了一个数据库连接, 则自动使用这个连接,如果存在多个则会抛出异常。
- local_connection(engine_name=None, auto_transaction=False): conn
- 返回缓存的数据库连接。如果不存在,则创建。
auto_transaction
是用来控制 是否自动创建事务。 - Connect(engine_name=None): None
- 清除缓存的线程连接,保证下次再访问时可以重建连接。
- Begin(ec=None): transaction object
- 开始一个事务。如果存在线程连接对象同时如果不存在当前线程内的连接对象,则自动从连接池中取一个连接 并绑定到当前线程环境中。ec为数据库引擎对象名,如果没提供,则缺省为 ‘default’. ec也可以为连接对象。
- Commit(close=False, ec=None, trans=None)
- 提交一个事务。使用当前线程的连接对象。
- CommitAll(close=False)
- 提交所有线程事务。
- Rollback(close=False, ec=None, trans=None)
- 回滚一个事务。使用当前线程的连接对象。
- RollbackAll(close=False)
- 回滚所有线程事务。
- do_(sql, ec=None)
执行一条SQL语句。使用当前的线程连接。只有当使用非ORM的API时才需要使用它 来处理,比如直接使用SQLAlchemy提供的:select, update, delete, insert时,可 以这样:
from uliweb.orm import do_ result = do_(select(User.c, User.c.username=='limodou'))
多数据库连接¶
从 0.1 版本开始,uliorm 就开始支持多数据库连接了,多数据库连接在这里有两种涵义:
- 同类数据库的不同连接
- 不同类的数据库的不同连接
所以这里没有简单地使用多数据库的说法,而是采用多数据库连接的说法。
在uliorm中多数据库连接的支持分为以下几方面的内容:
- 数据库连接的定义,涉及到settings.ini的配置
- Model如何指定数据库连接,涉及到settings.ini的配置和Model的定义以及执行
- 语句执行以及事务的多数据库连接的支持,包括中间件的支持,线程连接的处理等
- 命令行多数据库的支持
数据库连接的定义¶
首先为了区分不同的数据库连接,并且方便地引用它们,每个连接都需要定义一个名字。
在没有特殊定义的情况下,总是会有一个 default
的连接存在。它就是使用原来的
数据库连接的定义。当需要定义其它的数据库连接时,可以在 ORM 下定义 CONNECTIONS
如:
CONNECTIONS = {
'test': {
'CONNECTION':'mysql://root:limodou@localhost/test2?charset=utf8',
'CONNECTION_TYPE':'short',
}
}
上面定义了一个名为 test
的连接。
定义好连接,在启动 Uliweb 项目时,系统会自动根据配置创建相应的引擎对象。并且在
orm
中会自动创建一个管理对象,名为: engine_manager
,它可以象一个dict一样
使用,是用来管理连接的。我们可以通过它得到每个连接的信息,包括配置信息和创建的
相关对象的信息,主要包含:
options 连接参数:
connection_string: 连接串
connection_args: 连接参数
debug_log: 是否调试
connection_type: 连接类型, long or short
engine 引擎实例。对应实际的数据库引擎对象,比如通过
sqlalchemy的 ``create_engine()`` 创建的对象
metadata 对应的MetaData对象,可以通过它获得对应的表信息
models 与之相关的所有的Model对象信息
比如想要获得 default 的连接对象:
engine = engine_manager['default'].engine
或者直接使用 get_connection
engine = get_connection(engine_name='default')
如果只是访问缺省的连接,可以将 default 使用None来代替,如:
engine = engine_manager[None].engine
engine = get_connection()
因此我们可以了解,一旦项目启动,定义的数据库的引擎对象将直接被创建。但是此时真 正用来与数据库通讯的连接对象还没有创建,它们将随着请求被自动创建和管理。
Model的连接设置¶
在设计uliorm的多数据库连接时我一直在想:多数据库连接在什么情况下会被使用呢? 它们又是如何被使用呢?如果设计时考虑过多,会使得开发变得困难,因此我假设了以下 使用的场景:
- 数据库表本身直接与不同的数据库连接相对应,它们不会混用。这可能是最简单的一种 情况了。在这种情况下,我们只要能定义出表与将要使用的引擎之间的关系就可以了。
- 数据库表本身可能在多个不同的数据库连接中使用。这样,我们不仅要定义一张表与 不同的数据库连接关系,还要在运行时指定当前使用哪个连接。
根据以上的假设,uliorm提供了静态配置和动态切換两种方式。
静态配置又分为:settings.ini配置和Model属性配置。
在Uliweb中,每张表如果要使用首先要在settings.ini中进行配置,原来的写法是:
[MODELS]
user = 'uliweb.contrib.auth.models.User'
现在的写法是:
[MODELS]
user = 'uliweb.contrib.auth.models.User', 'test'
user = 'uliweb.contrib.auth.models.User', ['default', 'test']
比原来多了一项,就是数据库连接名。如果可以同时在多个连接中使用,后面的连接将是 一个list值。原来的写法依然是有效的,如果不提供,则会认为使用Model属性的定义,如 果Model属性定义也没有,则认为使用 default 连接。
在Model属性中也可以配置,就是添加 __engine_name__
属性,比如:
class User(Model):
__engine_name__ = 'test'
如果存在多种定义,那么uliweb将按以下顺序来处理:
- 是否设置了
__engine_name__
- 是否在
settings.ini
中设置了对应的连接名 'default'
所以缺省情况下是使用 default
。
当一个Model设置了多个连接名,要么在运行时动态指定,要么uliweb会抛出异常。
所以为了动态指定,uliorm的许多函数和方法都添加了 engine_name
参数,比如:
Model.connect(engine_name)
Result.connect(engine_name)
其中Model类上可以直接调用 connect()
来切換连接,它会直接影响后面的结果处理,包括
结果集的处理。这里 engine_name
还可以是 Engine
对象或 Connection
对象。
同时,当返回一个结果集时,在没有获得数据之前,也可以使用结果集的 connect()
来切換连接。
这种做法只会影响执行结果。
Note
原来想实现隐式的连接切換功能,即不要显示地使用象 connect()
这样的方法。但是
发现很难做到。
多数据库下的语句执行与事务处理¶
在数据库处理中,所有的语句都需要在连接上被执行,事务也是在连接上被处理。不同的 连接意味着不同的处理。考虑到web处理和批处理的方式不同,我们可以考虑以下的场景:
web处理一般是按请求来执行的,因此一个请求过来,创建一个连接,处理完毕后释放。 连接可以是长连接或短连接。长连接意味着将使用连接池,因此所谓的释放就是放回池 子里供下一次使用。而短连接就没有池子,释放就是真正的关闭,下次请求将再次创建。 而不管长连接还是短连接,处理模式都基本相同。
为了简化处理,我们可以每次当请求进入时自动创建一个连接,然后启动事务,并且把 这个连接放到线程环境中,这样所有使用
do_
就可以直接利用这个共享的连接和事务 了。这样的处理只是为了简化。因为有可能一个请求并没有事务处理,甚至不涉及到数据 操作,这样做有些过头了,不过目前为了简化,uliweb就是这样设计的。当支持多数据库连接时,情况有了一些变化。原来可以只自动建一个连接,但是现 在有可能是有多个连接。那么我们要为所有的连接创建实例,并启动事务吗?因此, 现在的策略就是只为缺省的连接创建连接实例,并启动事务。对于其它的连接, Uliorm 増加了一个名为
AUTO_DOTRANSACTION
的配置项,缺省为True
. 它的作用就是当你执行do_
时自动创建连接并启动事务。另一种做法就是使用Begin(ec=engine_name)
来手工创建连接和事务。目前只要是基本的 SQL 语句 ,包括: select, update, insert, delete 都是封装到了do_
中了。而象create
之类的是直接绑定到某个 engine 上,无法直接使用do_
, 所以 自动创建连接和事务一般还是可行的。象建表目前不建议自动创建,所以都是在命 令行上来执行的,它们都有特殊的处理。同时在处理完毕后,也不能只关闭和提交缺省的连接了,需要对所有创建的连接(包括 自动创建的连接)执行事务提交和关闭。
不过这些已经通过修改middle_transaction完成了。所以在简单情况下用户不用过份关心 这些细节。并且这种做法是兼容只有一个数据库连接的情况。
命令行和批处理情况有简单的也有复杂的。简单的情况和web请求的处理类似,也可以在 开始创建相应的连接和事务,在处理完毕后关闭。复杂情况下也可以自已手工创建连接和 启动事务。目前在命令行处理时有几个关键点:连接获取,Model的获取。Uliorm是完全 支持脱离WEB环境来使用的。因此我们可以象test_orm.py中那样,自已去创建连接, 创建Model,然后创建表。在这种情况下,Uliweb启动时做的自动化处理全部无效了,比 如缺省的
AUTO_DOTRANSACTION
的设置, 缺置的Begin
启动事务等。所以我们要自已去 启动事务。缺省情况下是自动提交的,所以每执行完一条SQL语句就会生效。同时uliweb还支持通过调用 make_application 或 make_simple_application 来启动 应用的实例。后者是专门为命令行准备了,除了个别的参数不能设外,如:debug,其它的 都一样。一旦启动,你的开发就和WEB区别不大了。所以缺省情况下
AUTO_DOTRANSACTION
是为True
的。因此你执行do_
时会自动启动事务。但是因为它没有 middleware_transaction 的封装,所以无法在处理完成后自动提交或回滚。这样如果你自已不处理,结果将无法 保存。对于这种情况,要么我们直接手工启动事务,以明确的事务方式来工作。要么执行set_auto_dotransaction(False)
来关闭自动生成事务,从而进入 autocommit 状态。 所以这点要比较注意。建议在命令行处理时,都主动使用事务。Note
现在在
make_simplae_application
中増加了启动时自动将AUTO_DOTRANSACTION
关闭的设置。所以使用它来启动应用环境直接就是autocommit
的状态。
前面说了,在使用 do_
和 Begin
时可以自动在创建线程共享的连接。在Uliorm
中维护着一个Local的对象,它上面有 conn
和 trans
对象,它们各是一个dict
分别保存着线程相关的连接和事务对象。在调用 do_
和 Begin
时会先检查是否
存在相应的连接和事务对象,如果存在,则直接使用,如果不存在,则创建。这里,还可以
分别传入 engine_name 参数,用来指明检查某个连接名相关的对象是否存在。线程相关的
连接和事务对象存在的目的是为了编程方便。如果所有的SQL都使用 do_
会比较简单。
但是因为 Model 把底层SQL的执行封装到了不同的方法中,所以要么它会自动使用 Model
配置的连接名来获得线程连接对象,要么你通过 connect(engine_name) 切換到其它的连接
名上,以便可以获得其它的线程连接对象,目前也可以传入一个真正的连接对象或Engine对象。
命令行对多数据库的支持¶
为了支持多数据库,在所有数据库相关的命令上都増加了 --engine
参数,可以用来
切換连接名。缺省是使用 default
。影响较大的是 dump*
和 load*
系列的函数.
原来数据库的数据文件是缺省放在 ./data
目录下的。现在为了支持多数据库,将会在
它的下面按连接分别创建子目录, 如: default
等。所以一旦使用了多数据库支持的
版本,原来的备份和数据装入的路径就发生了变化。
信号处理¶
uliorm提供类似django信号的处理机制,它会在一些重要的执行点调用发出信号,以便让 其它的信号处理函数进行后续的工作。注意,uliorm的信号并不是真正的异步,它只是定 义上的异步,调用还是同步的。
预定义的几种信号¶
uliorm已经提供了几种预定义好的信号,下面列举出来。在每个信号名的冒号后面所定义 的是使用dispatch调用时使用的方法,分为call和get。其中call不需要返回值,并且会 将所有订阅此信号的方法依次调用。而get需要一个返回值,一旦某个方法返回非None的值, 则结束调用并将值返回。
- pre_save:call
保存一个对象 前 发出的信号
参数: instance, created, data, old_data
- instance
- 为保存的对象
- created
- True为创建,False为修改
- data
- 新的数据
- old_data
- 旧的数据
- post_save:call
- 保存一个对象 后 发出的信号。参数同
pre_save
- pre_delete:call
删除一个对象 前 发出的信号
参数: instance
- instance
- 为待删除的对象
- post_delete:call
删除一个对象 后 发出的信号
参数: instance
- instance
- 为待删除的对象
- get_object:get
通过Model.get()获得一个对象 前 发出的信号。get_object和set_object 相结合可以实现简单的对get()方式的单对象的缓存处理。在uliweb中已经提供了一个 名为objcache的app,它可以在获取简单条件的对象时自动进行缓存的处理。
参数: condition
- condition
- 调用get()方法所使用的条件,它是SQLAlchemy的一个表达式对象
- set_object:call
通过Model.get()获得一个对象 后 发出的信号
参数: condition, instance
- condition
- 调用get()方法所使用的条件,它是SQLAlchemy的一个表达式对象
- instance
- 所获得的对象实例
定义接收函数¶
当使用uliorm时,它会根据执行情况自动发出相应的信号,此时如果有订阅此信号的方法存 在则将被自动调用,如果不存在,则继续后面的处理。在uliweb中,一般将订阅方法写在 settings.ini中,以减少启动时的导入处理。举例如下:
[BINDS]
audit.post_save = 'post_save'
audit.pre_delete = 'pre_delete'
在settings.ini中定义BINDS节,然后key是方法路径,值是对应的信号。方法路径的形式为:
module.function_name
为什么要这样定义?因为一个信号可以被多个方法来订阅,因此信号是可以重复的。
Uliweb在启动时会自动读取settings.ini中的信号,然后将其与相应的信号进行绑定。相 关的处理方法此时并不真正导入,而是当发出信号时,再动态导入。
接收函数的定义形式为:
def receiver(sender, topic, **kwargs)
第一和第二个参数都是固定的,sender是发出信号的对象。在uliorm中都是Model类。 topic是信号的名称。后面的kwargs对应每个信号可以接受的参数。不同的信号所接受的 参数可能是不同的。
测试代码¶
在 uliweb/test/test_orm.py 中有一些测试代码,你可以查看一些例子来了解如何使用 uliorm。
F&Q¶
如何处理Mysql中的 “MySQL server has gone away” 错误?¶
出现这个问题是因为Mysql有关于非活动连接超时断开的设置,缺省为8小时。当8小时以后 现有的连接没有活动,则MySql会自动断开。因此再次访问时会抛出这个错误。uliorm 使用SQLAlchemy的缺省的连接方式,会自动使用连接池。默认是5个连接。它有一个pool_recycle 的参数,用于设置回收连接的时间。这样,只要你设置一个小于MySql断开的超时时间就 可以了。示例如下:
[ORM]
CONNECTION_ARGS = {'pool_recycle':7200, 'echo_pool':True}
上述配置表示:连接池回收时间为7200秒(2小时)。echo_pool为True表示在日志中显示 回收信息。这样是通过自动回收重建连接池避免了这个问题。
MySQL 编码设置¶
在MySql中创建表时,uliorm将缺省使用utf8编码来创建,即使MySql的缺省编码不是utf8。 所以如果你使用的是MySql,你应该检查schema的缺省编码是不是utf8,如果不是则应该在 connection连接串上添加charset信息,如:
[ORM]
CONNECTION = 'mysql://root:limodou@localhost/new?charset=utf8'
当服务器的缺省编码不是utf8时, charset=utf8
是必须的,其它情况下可以不设置。
如何实现update table set field = field + 1类似的更新¶
举例如下:
User.filter(User.c.id==1).update(score=User.c.score+1)
或
User.filter(User.c.id==1).update(User.c.score=User.c.score+1)
或者使用底层的SQLAlchemy的写法:
do_(User.table.update().where(User.c.id==1).values(score=User.c.score+1))
如何实现MySql中区分大小写字段定义和查询¶
MySql在定义字段和查询字段时,缺省是使用非大小写敏感方式进行处理的。有时我们需要 进行大小写敏感方式的查询,因此这里涉及两种处理,一种是查询时的大小写区分,如:
from sqlalchemy.sql import func
User.filter(User.c.username == func.binary('limodou'))
上述代码将按大小写对’limodou’进行查询。
但是如果你把CHAR或VARCHAR设置为不重复的索引,在插入类似: Limodou
或 limodou
有可能会报重复。这就不是靠查询来解决的了。要通过将字段定义为区分大小写的形式。在
MySql中一般是在VARCHAR之后添加Binary,如:
username VARCHAR(40) binary
那么在Uliorm或SQLAlchemy中如何做呢?代码如下:
from sqlalchemy.dialects.mysql import VARCHAR
class Human(Model):
name = Field(str, verbose_name='姓名', max_length=40, required=True)
login_name = Field(str, verbose_name='登录名', required=True,
max_length=40, unique=True, type_class=VARCHAR,
type_attrs=dict(binary=True))
可以看到它使用了mysql的dialect的字段定义,并将其传入uliorm的字段定义中,其中参
数 type_class
为字段类型, type_attrs
为字段相应的参数,这里设置 binary
为 True
。在SQLAlchemy中的定义示例如:
from sqlalchemy.dialects.mysql import VARCHAR
Column('username', VARCHAR(40, binary=True))
这样在数据库中,就是区分大小写的,在查询时不再需要使用func.binary()来处理了。
不过这种方式兼容性不好,所以还有一种变通的方式就是写一个sql文件,在命令行下对 字段进行修改,这样Model就不需要修改了。比如:
use <database>;
ALTER TABLE human MODIFY COLUMN `login_name` VARCHAR(40)
BINARY CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL;
RuntimeError: dictionary changed size during iteration¶
在Uliweb下使用uliorm,要求将所有的Model都定义在settings.ini中,一旦出现某个Model 没有在settings.ini中定义,就有可能出现上面的问题。
反向获取ManyToMany关系时,找不到对应属性¶
在Uliweb中,如果两个表存在ManyToMany关系,则关系一般只会定义在其中一个Model类上 被定义。例如有两个Model: A和B。在A上定义了一个到B的ManyToMany的关系。在导入A类 时(或通过get_model来获取)会自动向B类绑定一个反向获取的对象,用于从B的对象获得A对 象时使用。因此,有时候,你直接导入B类,但是因为B类中没有定义与A的任何关系,所以 对A的反向获取对象将无法生成,因此可能不能直接使用B到A的反向获取。在这种情况下,你 可以再使用get_model或导入A,这样就可以生成反向获取对象了。
模板(Template)¶
Uliweb目前内置的模板系统采用web2py的,所以语法与web2py的模板系统完全一样。在原基础上进 行了必要的修改。
特点¶
- 简单,易学
- 可以嵌入Python代码
- 不用过分关心Python代码的缩近,只要注意块结束时加pass,模板会自动对缩近进行重排
- Python代码与HTML可以交叉使用
- 支持模板的继承
- 支持类django的block的功能(此功能由Uliweb扩展)
- 提供一些方便的内置方法
- 支持环境的扩展(可以扩展可以直接在模板中使用的对象和方法)
- 先编译成Python代码,然后再执行
基本语法¶
Uliweb的模板的语法很简单,只有四种类型的标记:
{{= result}}
这是用来输出的标记。其中result可以是变量也可以是一个函数调用,它会自动 对输出的内容进行转义{{<< result}}
这是用来输出非转义的内容,与上面相反。{{ }}
只使用{{}}的话表示里面为Python代码,可以是任何的Python代码,如:import之类的。 如果是块语句,需要在块结束时使用pass。{{extend template}}
其中template可以是字符串,如"layout.html"
或变量。它表示从 父模板继承。{{include template}}
包括其它的模板。如果template省略,表示是子模板插入的位置。一个 父模板只能定义一个插入点。{{block blockname}}{{end}}
用于定义一个块。在子模板中,对于要覆盖的block需要进行定义, 则生成的结果将用子模板中的block定义替换父模板的。
模板环境¶
Uliweb的模板在运行时也象view一样会运行在一个环境中,在这个环境中,有一些对象和方法是在内置环境中定义的,也有一些是在Uliweb的框架环境中定义的对象或方法。内置的环境你无法扩展,但是框架环境允许你扩展,方法很象view的扩展方式,如在任何一个有效的app的settings.py中可以定义:
from uliweb.core.dispatch import bind
@bind('prepare_view_env')
def prepare_template_env(sender, env):
from uliweb.utils.textconvert import text2html
env['text2html'] = text2html
经过上面的处理,你就可以直接在模板中使用text2html这个方法了。
目前已经有一些缺省的对象和方法可以直接用在模板中,它们目前与view是一样的,因此你可以参考 视图 文档进行了解。
out 对象¶
out 对象是模板中内置的用来输出文本的对象。你可以在模板中直接使用它,但一般是不需要的。它有以下的方法:
- write(text, escape=True) 输出文本。escape表示是否要进行转义。
- noescape(text) 输出不转义的文本。
编码¶
模板缺省是使用utf-8编码进行处理。如果你传入unicode字符串,将自动转为utf-8编码。如果不 是unicode,则不做处理。建议全部使用utf-8编码。
基本用法¶
- 简单地变量输出
{{= "hello"}}
{{= title}}
如果使用了变量,则要么由view进行传入,要么在模板的其它地方进行定义,如:
{{title="hello"}}
{{= title}}
- HTML代码直接输出
{{<< html}}
- Python代码示例
{{import os
out.write("<h1>Hello</h1>")
}}
- 模板继承
父模板 (layout.html)
<html>
<head>
<title>Title</title>
</head>
<body>
{{block main}}{{end}}
</body>
</html>
子模板 (index.html)
{{extend "layout.html"}}
{{block main}}
<p>This is child template.</p>
{{end}}
同时 extend 支持后面的模板文件名是一个变量,如:
{{extend layout}}
这样,你可以在渲染模板时,传入一个layout的变量。
另外,在复杂的情况下,可以是多级的模板继承关系,如:
/ extend C1
A extend B - extend C2
\ extend C3
也就是说,C1, C2, C3作为父模板,可能有一些相同的block的定义可以扩展,如果它们可重定义的block一样,并且在你的应用中希望统一进行预处理,然后其它的模板再使用这个预处理后的模板。那么可以采用一个新的方法,首先B模板定义为:
{{extend layout}}
即,它要继承的模板名是一个变量。然后在A中根据需要传入layout的值,如:
{{extend "B", layout="C1"}}
这样,在扩展B模板时,动态传入了layout变量的值,因此B中的layout将使用C1模板。
下面的include也有类似的功能 。
- 包括其它的模板
<html>
<head>
<title>Title</title>
</head>
<body>
{{include "child.html"}}
</body>
</html>
uliweb.contrib.template App¶
Uliweb为了方便使用,同时还提供了 uliweb.contrib.template 这个app,具体的功能描述见 uliweb.contrib.template 。
URL映射¶
Uliweb使用Werkzeug的Routing来进行URL的处理。当你使用manage.py的makeapp命令生成一个新 的App时,它会自动生成views.py文件,其中会自动从uliweb.core.SimpleFrame中导出expose 函数,它是一个decorator函数,用于修饰view函数。
通过expose可以将一个URL与一个view函数进行绑定,然后通过url_for(这是SimpleFrame提供的用 于反向生成URL的方法)来生成反向的URL。
expose说明¶
目前,Uliweb取消了集中的URL配置,因此你需要在每个view方法前加上expose()来定义URL。 但同时,Uliweb还允许你将URL定义在settings.ini,以方便实现URL的替換。
Uliweb目前提供两种View函数的写法,一种是简单的函数方式,另一种是类方式的定义,下 面分别进行描述。
普通View函数的处理¶
基本用法为:
缺省映射
@expose() def index(arg1, arg2): return {} @expose def index(arg1, arg2): return {}
当expose()不带任何参数(也可以不带括号)时,将进行缺省的映射。即URL将为:
/appname/view_function_name/<arg1>/<arg2>
如果view函数没有参数,则为:
/appname/view_function_name
固定映射
@expose('/index') def index(): return {}
参数处理
当URL只有可变内容,可以配置为参数。一个参数的基本形式为:
<convertor(arguments):name>
其中convertor和arguments是可以缺省的。convertor类型目前可以设置为:int, float, any, string, unicode, path等。不同的convertor需要不同的参数。详情请参见 下面的converter说明。最简单的形式就是<name>了,它将匹配/到/间的内容。
name为匹配后参数的名字,它需要与绑定的view方法中的参数名相匹配。
其它参数
expose函数允许在义时除了给出URL字符串以外再提供其它的参数,比如:
defaults
它用来定义针对view函数中的参数的缺省值,例如你可以定义:
@expose('/all', defaults={'page': 1}) @expose('/all/page/<int:page>') def show(page): return {}
这样两个URL都指向相同的view函数,但由于show方法需要一个page参数,所以对于第一 个/all来说,需要定义一个缺省值。
build_only
如果设置为True,将只用来生成URL,不用于匹配。目前Uliweb提供了静态文件的处理, 但一旦你想通过象Apache这样的web server来提供服务的话,就不再需要Uliweb的静态 文件服务了。但是有些文件的链接却是依赖于这个定义来反向生成的,因此为了不进行匹配, 可以加上这个参数,这样在访问时不会进行匹配,但是在反向生成URL时还可以使用。
methods
HTTP请求可以分为GET, POST等方法,使用methods可以用来指定要匹配的方法。比 如:
@expose('/all', methods=['GET'])
关于参数更多的说明请参见werkzeug下的routing.py程序。
在settings.ini中定义URL¶
Uliweb也支持将URL定义到settings.ini,其主要目的是为了允许别人替換。比如已经开 发了一个app,有一些常用的URL的定义。但是希望别人可以替換已经定义好的URL,如果 直接写到views中,则不会进行替換,只会添加。所以放到settings.ini中就可以方便替 換了。定义示例如下:
[EXPOSES]
login = '/login', 'plugs.user.views.login'
logout = '/logout', 'uliweb.contrib.auth.views.logout'
register = '/register', 'uliweb.contrib.auth.views.register'
Key是URL的名字,值一般是二元或三元的tuple。形式为:
(url_pattern, view_function_path[, kwargs])
第一个为url模式,第二个为url对应的view函数的路径,第三个是可选的,应该是一个字典, 它是将传入expose中的参数。
GET和POST¶
为了方便处理expose(methods=[‘GET’, ‘POST’])这样的URL,uliweb还定义了GET和POST, 分别用于处理GET和POST方法,其它的象DELETE要象上面这样定义。
与decorator联用时的注意事项¶
有时我们希望通过使用decorator来修饰view方法,包括类的view方法。那么由于expose 本身也是一个decorator,并且当函数有参数时,在expose不传入参数时,将自动对函数 的参数进行解析,而decorator的处理方式,有可能会造成新生成的方法与原始的方法参 数不同,会使得生成的URL出现问题。因此对于普通的view函数,建议将expose放在最下 面,以保证expose先执行。而在使用类view方法时,对于只有self参数的简单方法,可以 只加decorator,并且使用自动URL的处理。但对于带有除self之外的其它的参数,使用自 动URL处理可能会出现问题,因此建议添加expose的修饰,并且放在其它的decorator之上, 如:
@expose('/myview')
class MyView(object):
@_other
def test1(self):
#这个可以
@_other
def test2(self, id):
#这样可能有问题,因为_other有可能创建新的函数,造成与test2的
#参数不同
@expose('test3/<id>')
@_other
def test3(self, id):
#正确,添加显示的expose调用,并且使用相对URL的定义,以便和
#缺省URL的处理一致
@_other
@expose('test3/<id>')
def test3(self, id):
#可能不正确
url_for说明¶
url_for可以根据view方法的名字来反向生成URL。要注意,它需要一个字符串形式的view方法名, 格式为:
url_for('appname.views_module_name.function_name', **kwargs)
其中kwargs是与view方法中的参数相对应的。例如你在Hello中定义了如下URL:
@expose('/index')
def index():
pass
然后在反向生成URL时可以使用:
url_for('Hello.views.index') #结果为'/index'
如果你在运行时希望可以动态适应App名字的变化,可以使用:
url_for('%s.views.index' % request.appname)
其中request是请求对象,它有一个appname的属性表示访问的App的名字。
Note
目前在views方法和template中都是可以直接使用这个函数的,不需要导入。
convertor说明¶
int
基本形式为:
<int:name> #简单形式 <int(fixed_digits=4):name> #带参数形式
支持参数有:
- fixed_digits 固定长度
- min 最小值
- max 最大值
float
基本形式为:
<float:name> #简单形式 <float(min=0.01):name> #带参数形式
支持参数有:
- min 最小值
- max 最大值
string 和 unicode
这两个其实是一样的。
基本形式为:
<string:name> <unicode(length=2):name>
支持的参数有:
- minlength 最小长度
- maxlength 最大长度
- length 定长
path
与string和unicode类型,但是没有任何参数。就是匹配从第一个不是
/
的字符到跟着的字 符串或末尾之间的内容。基本形式为:<path:name>
举例:
'/static/<path:filename>'
可以匹配:
'/static/a.css' -> filename='a.css' '/static/css/a.css' -> filename='css/a.css' '/static/image/a.gif' -> filename='image/a.gif'
any
基本形式为:
<any(about, help, imprint, u"class"):name>
将匹配任何一个字符串。
视图(View)¶
在Uliweb中,视图(view)相当于MVC框架中的Controller。
view模块的定义¶
在Uliweb中,在一个app的目录下,所有以views开头的文件都将被视为视图模块。当你不使用集中的 URL管理的时候,Uliweb会自动将所有有效的app的视图文件在启动时进行导入,其目的就是为了搜集 所有的URL的定义。因此,只要你按规则进行定义文件名,在其中定义的URL就可以被自动发现。因此像: views.py, views_about.py都是合法的view模块。
基于函数的View方法¶
view函数的定义¶
在Uliweb中,一个view函数可以简单地定义为:
- def index():
- pass
可以看到,它就是一个普通的函数。目前对于view函数,你只能使用普通的函数,而不能使用类。 每个view函数都应与一个或多个URL定义相匹配,一个完整的view定义如下:
@expose('/index')
def index():
pass
如果一个view函数没有使用expose来修饰的话,它将不会被用户所访问。
expose后面是可以没有参数的,如:
@expose
def index():
pass
那么这个时候,一个view函数的URL将被定义为:
/Appname/view_module_name/view_function_name
它是由app的名字,view模块名和view函数名组成的。这就是缺省的URL映射,已经很象是MVC的缺 省映射了。
view函数的参数¶
view函数是可以有参数的,但首先你需要先在它的URL中定义参数,如果URL中有参数,则view中就 要定义参数,如果没有则view中一般也没有。带参数的例子:
@expose('/documents/<lang>/<path:filename>')
def show_document(filename, lang):
return _show(request, response, filename, env, lang, False)
关于URL的参数定义,参见 URL映射 的文档。这里可以看出,定义了两个参数: lang和filename,所以在下面的view函数中也定义了两个参数。
但如果你的URL中没有那么多的参数怎么办?这个可以在URL的定义中解决,如:
@expose('/documents/<path:filename>', defaults={'lang':''})
@expose('/documents/<lang>/<path:filename>')
def show_document(filename, lang):
return _show(request, response, filename, env, lang, False)
则在第一个URL的定义中,只有一个filename参数,因此可以使用defaults来定义缺省参数。
view的环境¶
在Uliweb中,一个view函数是运行在某种环境中的,当需要调用view函数时,在调用前,我会向 函数的func_globals属性中注入一些对象,这些对象就可以直接在函数中使用了,你不再需要导入。 目前可用的对象有:
- application 这是Uliweb的实例,你可以用它来访问应用的各种属性,如:application.debug 表示是否处于调用状态,还可以通过它来调用一些方法,如:application.template()来处理模 板。当然直接导入template也是可以的。不过application.template()已经预设了环境进去。
- request 请求对象。
- response 应答对象。这个对象在传入时是一个空对象,你可以使用它,也可以自行构造一个Response 的对象进行返回。
- url_for 它是与expose是相反的,它用来生根据view函数生成反向的URL。详情见 URL映射 的文档。
- redirect 用于重定义处理,后面为一个URL信息。
- error 用于输出错误信息,它将自动查找出错页面。你只要在任何app下的templates中增加 error.html,然后出错信息可以自已来定制。它也不需要前面加return,也将抛出一个异常。
- settings 是定义在所有有效的app settings.py文件中的配置项。注意,一个配置项的名称必须是 大写的。
- json 用于将dict对象包装成json格式并返回。
Note
要注意,以上的环境只能用在view函数中,当view调用其它的方法时,还是需要传入相应的参数。 有些全局性的对象将放在 uliweb/__init__.py 中,因此可以直接导入。详情见 全局环境 的文档。
view环境的扩展¶
如果你认为上面的环境还不够,那么你可以直接向env中增加新的对象,然后在view方法中可以通过 env.object的方式来使用它。你需要在某个app的settings.py文件中增加相应的插件处理。如:
from uliweb.core.dispatch import bind
@bind('prepare_default_env')
def prepare_default_env(sender, env):
from uliweb.utils.textconvert import text2html
env['text2html'] = text2html
Uliweb中已经定义了 prepare_default_env
这个plugin的插入点,你可以直接使用它。它的
作用就是向env中增加新的对象,如上面是增加了一个新的函数可以用来将文本转为HTML代码。
view的返回¶
在Uliweb中,view函数可以返回多种类型的结果。可能为:
- dict 变量。如果返回一个dict的变量,说明你希望由Uliweb自动套用一个模板,这个模板需要在 templates目录下,并且模板的文件名需要与view函数名相同,后缀为.html。如果你希望使用指 定的模板文件,则需要利用response对象,将指定的模板名赋给response.template属性就行了。
- response 对象。记得上面说过的吗?你可以直接使用response对象,比如调用它的response.write() 方法来写入返回的内容。
- 字符串。你可以直接返回一个字符串,这样将被封装为一个普通的文本返回。
- json 对象,使用前面讲的json函数对dict对象进行包装。
- Reseponse实例。你可以主动创建一个Response的实例并返回。
在某些情况下,你可以调用象redirect, error来中止view的运行。
view模块的入口处理¶
我建议将不同的view函数按照功能和处理分为不同的文件来存放。
Uliweb支持一种view模块的入口和出口的处理。即你可以在view模块中定义名为 __begin__
和
__end__
的特殊的方法,它没有参数,但是就象普通的view函数一样,也是在view环境中运行的。
一旦view模块中存在这个特殊的方法,在执行每个view函数之前都会先调用这个函数。因此你可以
把它理解为初始化处理,比如给一些对象赋值。举例如下:
def __begin__():
from uliweb.contrib.auth.views import login
if not request.user:
return redirect(url_for(login) + '?next=%s' % url_for(doto_index))
基于类的View方法¶
目前Uliweb也支持类的方式来定义view,这样可以有更好的封装性。举个例子:
@expose('/user')
class UserView(object):
def __begin__(self):
if not request.user:
return redirect('/login?next=%s' % request.path)
@expose('/login')
def login(self):
#login process
#URL = /login
def register(self, name):
#register process
#URL = /user/register/<name>
@expose('add')
def add_user(self):
#add process
#URL = /user/add
@expose('')
def list(self):
#URL = /user here '' will just user UserView class URL prefix '/user'
def _common(self):
#this function will not be exposed
以上只是一个示例。使用基于类的view除了是以类的方式进行组织外,与一般的view方法 没有本质的区别,但同时又有一些其它的特性。
- 建议使用New Style Class,即从object派生。不需要从特殊的基类派生。
- Class-View也支持类似模块级别的__begin__的处理,但它是一个方法。Uliweb在处理 Class-View会自动调用。
- 不要使用staticmethod对类方法进行处理。因此类方法可以是一般的方法或classmethod。
- 如果方法名开始为’_’,则这个方法将不会被自动exposed,客户端将不能进行访问。这种 方式比较适合定义内部的函数。
- 如果使用Class-View并且要在Class上使用expose,你需要安装Python 2.6。
- 在简单情况下,你在类上使用expose(‘/url’),而类的方法上不使用expose,则会自动对有效的 方法生成形如:/url/method_name 的链接形式,如果还带参数,则自动生成字符串形式 的参数。例如上面的register函数。在注释中可以看到。
- 在上面的例子中已经演示了大部分的情况:
- 类上使用expose
- 覆盖自动URL的生成,如login()方法。因为这里使用了’/login’,相当于绝对路径。
- 定义相对URL,如add()方法。
- 使用缺省expose方式,同时定义了参数,如register()方法。
- expose(‘’)将直接使用类上的URL。
- 内部函数,不会被客户端访问到,如_common()方法。
- 定义了__begin__()方法,可以在执行类方法前先被处理。
Note
注意,一般不要定义__init__()。因为Uliweb在调用Class-View时,会自动创建类的 实例,如果定义__init__()则不要带参数或全部使用缺省值。
Note
注意,如果在方法上还想使用decorator来进行修饰,如果方法无参数,则顺序不影响 最终的URL生成。如果方法有参数,建议不要使用缺省URL的生成方式,而是主动定义 expose,并且将expose放在所有其它的decorator之上。
Form使用¶
在编写Web应用中,经常要使用到的就是和用户的交互,在传统的HTML开发中,一般是使用 Form来进行的,它通过一组Form相关的界面元素,提供各种信息的录入。在ajax的处理过 程中,Form元素也经常被使用。除了前端的展示外,真正的交互是需要后台参与的,它包 括上传数据的解析和处理,然后可能会与数据库进行交互,并返回相应的结果。在Uliweb 中,提供了Form类来进行相关的处理,它的主要功能有:
- 自动生成前端的展示代码,允许支持自定义布局
- 对上传后的数据进行校验,如果正确则返回转換后的数据,如果出错,返回出错信息
Form的定义¶
在使用Form时,我们做的第一件事就是定义一个Form类,定义之后,我们会创建它的实例, 然后使用这个实例来展示或进行数据的校验处理。一个简单的Form类定义代码如下:
from uliweb.form import *
class F(Form):
title = StringField(label='中文:', required=True, help_string='Title help string')
content = TextField(label='Content:')
password = PasswordField(label='Password:')
age = IntField(label='Age:')
birthday = DateField(label='Birthday')
id = HiddenField()
tag = ListField(label='Tag:')
public = BooleanField(label='Public:')
format = SelectField(label='Format:', choices=[('rst', 'reStructureText'), ('text', 'Plain Text')], default='rst')
radio = RadioSelectField(label='Radio:', choices=[('rst', 'reStructureText'), ('text', 'Plain Text')], default='rst')
file = FileField(label='file')
上面的代码定义了一个Form类,里面有很多的字段,类似于Model的定义。在uliewb/form/uliform.py
中定义了许多Form字段类,分别代表不同类型的字段。所有的字段都继承自 BaseField
类,对
于BaseField类的详细说明见下面。
BaseField¶
1 2 3 4 5 6 7 8 9 10 | class BaseField(object):
default_build = Text
field_css_class = 'field'
default_validators = []
default_datatype = None
creation_counter = 0
def __init__(self, label='', default=None, required=False, validators=None,
name='', html_attrs=None, help_string='', build=None, datatype=None,
multiple=False, idtype=None, static=False, **kwargs):
|
Middleware 开发¶
Uliweb中的Middleware是类似于django的Middleware,它可以在交易处理前和处理后,以 及出错时执行Middleware中的方法,起到一种通用的中间件的作用。Middleware是基于请 求的,它和平时的wsgi middleware不同,wsgi middleware是基于应用级的,当然也可以 处理请求,但是比Middleware还要底层,本文就不讨论了。
介绍¶
先以contrib.auth.middle_auth的AuthMiddle为例:
from uliweb import Middleware
class AuthMiddle(Middleware):
ORDER = 100
def process_request(self, request):
from uliweb.contrib.auth import get_user
request.user = get_user(request)
一个Middleware要从 Middleware
类派生。一般只需要定义三个方法:
process_request(request)
process_response(request, response)
process_exception(request, exception)
不同的Middleware可以根据需要分别定义不同的方法。
Middleware基类有一个缺省的 __init__
方法,如:
def __init__(self, application, settings):
self.application = application
self.settings = settings
你也可以自已定义一个,以便进行初始化的处理。
执行顺序¶
从上面的示例中,可以看到AuthMiddle中定义了一个 ORDER
的属性。Uliweb在调用
middlware时会根据 ORDER
的值先对Middleware进行排序,然后再根据顺序进行依次
调用。这里的顺序只是缺省的顺序,在用户进行配置时还可以进行修改。详见下面的配
置说明。
调用逻辑¶
下面列出执行的伪代码进行说明:
#排序
middlewares.sort()
#执行Middleware,执行process_request
for m in middlewares:
if hasattr(m, 'process_request'):
res = m.process_request(request)
if res is not None:
return res
#调用view方法
try:
res = call_view()
except Exception, e:
for m in reversed(middlewares):
if hasattr(m, 'process_exception'):
res = m.process_exception(request, e)
if res is not None:
break
#继续抛出异常
raise
#执行Middleware, 执行process_response
for m in reversed(middlewares):
if hasattr(m, 'process_response'):
res = m.process_response(request, res)
- 先是对Middleware进行排序。
- 对所有的Middleware中的process_request进行处理。如果有返回值不是None,则跳出 循环。这里会对Middleware中是否有process_reqeust方法进行判断。后面类似。
- 然后是执行view方法,获得response。在执行中,如果出错,则按倒序对Middleware 进行处理。如果某个process_exception有返回值,则跳出循环。最后通过raise再次抛 出异常,让整个应用捕获。
- 如果没有异常,则按倒序执行Middleware中的process_response方法。这里和process_request 不同。你需要强制返回response对象,因为它会传递到下一个处理方法中。
配置¶
有两个地方可以配置:apps/settings.ini和某个应用下的settings.ini。
Uliweb缺省定义了一个空的section:
[MIDDLEWARES]
它的定义形式为:
middleware_name = 'middleware_class_path'[, order]
前面的key是middleware的名字。后面的值可以有两种写法, 一种是只有middleware的类路径, 另一种是在类路径的后面还有一个顺序号。如果没有给出,则uliweb会自动从middleware 类的属性中获取ORDER的值,如果不存在,则缺省置为500。如果值为空,则当前的middleware 将被删除。
MIDDLEWARE的顺序如何确定?
在前面伪代码中,有一个对Middleware进行排序的处理。它会根据Middleware中的ORDER的 大小进行顺序。如果顺序号相同,则保持导入的顺序。
Uliweb中的app有些已经提供了settings.ini中的MIDDLEWARES的定义,它们只要你在 INSTALLED_APPS中包含app即可使用。顺序一般也定义好了。
因此当你自已写了Middleware或特殊情况下,才需要重新定义顺序。
在Contrib中定义的Midddleware¶
下面列出在contrib中定义的一些Middleware供参考:
- ‘uliweb.contrib.auth.middle_auth.AuthMiddle’ ORDER=100 app=’auth’ 用于在请求进来时,向request添加一个user的对象。这样用户就可以直接通过request.user 来判断用户是否已经登录和得到登录用户对象。
- ‘uliweb.i18n.middle_i18n.I18nMiddle’ ORDER=500 app=’i18n’ 用于i18n的处理,设置语言类型
- ‘uliweb.contrib.session.middle_session.SessionMiddle’ ORDER=50 app=’session’ 请求进来时自动读取session。请求结束时自动保存cookie。
- ‘uliweb.orm.middle_transaction.TransactionMiddle’ ORDER=80 app=’orm’ 提供事务的支持。当view出错时,自动回滚,成功时自动提交。
所以,当你使用了上面几个app时,它会自动按:
'uliweb.contrib.session.middle_session.SessionMiddle'
'uliweb.contrib.auth.middle_auth.AuthMiddle'
'uliweb.orm.middle_transaction.TransactionMiddle'
'uliweb.i18n.middle_i18n.I18nMiddle'
的顺序来执行。
Generic 说明¶
Genric是什么?¶
在编写View相关的代码时,我们遇到最多的处理恐怕就是:列表显示、添加、删除、更新、修改 了,一般的叫法是CRUD(Create, Read, Update, Delete)这里没有List。那么Generic的目的 就是把这些常见的处理进行封装,并且它可以和Uliorm相结合,可以比较容易地对表中的 记录进行处理。在Uliweb的utils/generic.py中提供了上述的功能。
generic的整个设计思路是为了将处理程流进行复用。首先是根据按执行的功能分为不同的类。 然后将完整的处理流程封装到类中。但是在实际处理过程中,总会有各种各样的特殊的要求, 因此,你有两种扩展的方式:一种是派生新的类,另一种是将必要的参数和回调传入初始化 函数。一般的方式是采用第二种,因为这种方式相对简单。
generic主要是对Model的界面处理进行了自动化,所以它主要是和Model相结合使用。
在generic中,针对不同的处理提供了不同的View Class,下面分别进行介绍。
Note
本文档附带了一个示例,可以从 uliweb-doc/projects/genric_blog 中找到。
ListView¶
ListView用来处理列表显示。在最简单的情况下,你可能只需要在view中返回一个结果集, 然后在模板中对它进行展示。不过,这样一些处理将会集中在模板中。而ListView通过丰 富的参数,可以比较方便地进行:设置条件、处理字段、字段值的加工、不同的展示方式、 下载支持等。
ListView参数说明¶
class ListView(SimpleListView):
def __init__(self, model, condition=None, query=None, pageno=0, order_by=None,
fields=None, rows_per_page=10, types_convert_map=None, pagination=True,
fields_convert_map=None, id='listview_table', table_class_attr='table',
table_width=True, total_fields=None, template_data=None,
default_column_width=100, meta='Table', render=None):
上面是ListView的初始化函数的定义,可以看到它提供了大量的参数。同时用户也可以根 据需要从ListView类进行派生。ListView是从SimpleListView派生来的,它主要用来处理 与Model相关的列表展示,而SimpleListView主要是处理查询后的结果,不直接与Model 绑定。下面对每个参数进行说明:
- model
- ListView要绑定的Model,这个Model将是显示的主体。
- condition
- 查询条件。在执行时,ListView将会按 model.filter(condition)的形式来获得结果 集。
- query
- 结果集。如果用户传入了一个在model上的结果集,则它将结合condition条件,使用 query.filter(condition)来获得结果。这里就不再是model对象了,而是传入的query 对象。所以用户要保证这个query是操作model得到的结果集。
- order_by
对查询结果进行排序。它可以是排序字段的列表,写法要符合sqlalchemy的要求,比如:
(Model.c.name, Model.c.age.desc())
可以有多个排序字段,可以按升序或降序排序。
- pageno
- 页号。ListView支持分页查询。第一页是从0开始。
- rows_per_page
- 每页显示的记录条数。
- pagination
- 是否使用分页方式的标志。缺省为使用。如果为False则不使用分页方式。
- fields
- 用于传入需要显示的字段列表。如果没有给出,则自动使得后面的meta字段所指定的, 定义在Model中的特殊子类的fields属性。具体的参见下面的关于字段列表定义的说明。
- types_convert_map
- 用来定义字段类型与显示值处理函数的映射。具体说明,参见下面关于字段的展示的 说明。它同下面的fields_convert_map类似,只不过fields_convert_map只处理特定 名字的字段,只能是一个字段;而types_convert_map是处理特定类型的字段,可能是 多个字段。
- fields_convert_map
- 用来定义字段与显示值处理函数的映射。具体说明,参见下面关于字段的展示的 说明。
- id
- 生成页面时<table>元素的id属性名。
- table_class_attr
- 用于指明<table>元素的class属性值。
- table_width
- 是否指定表格以像素计算的宽度,如果是,则会根据每列的宽度进行计算总宽度,然后 设置表格的总宽度。
- default_column_width
- 缺省每列的像素宽度,缺省为100px。
- total_fields
- 用于合计字段的计算。
- template_date
- 将传入模板中的变量dict。
- meta
- 如果使用Model中的字段定义,则使用指定名字的子类中的fields属性。缺省为 ‘Table’ , 你可以指定其它的名字。
- render
- 如果不希望ListView按缺省的数据加工方法对数据进行处理,可以传入自定义的render
函数。它是一个回调,调用形式为:
render(record, obj)
,record为正在处理的记录, 它的值是一个二元的tuple,形式为:(name, display)
。obj为当前正在处理的对 象。
字段列表定义¶
在ListView中,用户可以有两种定义列表显示字段的方式:
- 通过fields字段,传入字段列表
- 通过在Model类中定义一个子类,来定义字段列表
第一种方法可以在运行时根据需要动态修改显示字段的列表,而第二种相对静态。代码示 例如下:
fields = ['name', 'age',
{'name':'plan_stat','verbose_name':'计划状态', 'width':80},
]
上面代码是在view代码中定义fields的示例。它支持简单的字段,即只列出字段名称。一 般这种情况下,字段名称在Model中应有对应的属性。比如上例中,应该在传入的Model 对象中有’name, ‘age’, ‘main_sys’这几个字段。对于复杂的字段,如上例中的dict方式 定义的字段,它主要是用于Model中不存在的字段,因此你需要定义以下几个属性:
- name
- 字段的名字,英文名
- verbose_name
- 显示用的名字。如果没有,则使用name值
- width
- 可选,这个是与生成的表格相关的。generic.py缺省可以提供使用<table>生成的清 单。也支持使用jquery easyui的datagrid生成的表格。这个参数是用来定义列的宽 度。缺省不定义的话宽度是100px。
- sortable
- 可选。这个也是与使用jquery easyui有关的,其它的情况下,要么你从ListView派生 新的子类,对生成<table>进行了处理,可以考虑定义它,如果不是,则没有什么用。
因此上面name和verbose_name一般是必须的,其它的根据需要来使用。并且,定义哪些值 还和将来展示时使用的包有关系,这块也可以自已去扩展。
第二种方法示例:
class Test(Model):
name = Field(str, max_length=30, verbose_name='姓名')
age = Field(int, verbose_name='年龄'
class Table:
fields = [
{'name':'name', 'width':150},
'age',
]
上面的定义也支持不存在的字段,支持简单定义和复杂定义。
执行流程描述¶
先给出代码示例:
def list(self):
from uliweb.utils.generic import ListView
def title(value, obj):
return obj.get_url()
view = ListView(self.model, fields_convert_map={'title':title})
return view.run()
从 generic 中导入 ListView 。
根据需要对需要传入 ListView 中的参数或回调函数进行处理
创建 ListView 实例
返回 view.run(),它将返回一个 dict ,包含内容为:
{'table':以table方式显示的表格数据, 'table_id':table的id, 'total':总条数, 'pageno':当前页号, 'page_rows':每页显示的条数 }
同时它还包含了传入到template_data中的数据。
所以在最简单的情况下,对应的模板可以写为:
{{extend "BlogView/layout.html"}}
{{block content}}
<a href="/add">添加Blog</a>
{{<< table}}
{{end}}
直接展示 {{<<table}}
即可。
字段转換¶
ListView中可以对某个字段的值进行转換,同时这种转換支持对不存在的字段进行处理。 这里要使用 fields_convert_map 这个参数,它是一个 dict ,key就是要转換的字段名, value是对应的转換函数。转換函数定义为:
def convert(value, obj):
其中value为对应字段的值,obj为对应的记录对象。你需要返回一个字符串。举例如下:
def title(value, obj):
return '<a href="/view/%d">%s</a>' % (obj.id, value)
fields_convert_map = {'title':title}
view = ListView(model, fields_convert_map=fields_convert_map)
这样就可以在显示 title 字段时调用 title() 函数返回一个链接。
不存在字段支持¶
如果是处理不存在的字段,第一步是在传入的 fields 中或在 class Table 中定义这个字 段的复杂方式,即至少要定义为一个dict,而且包含: name, verbose_name 属性。然后定义 一个convert函数,并且配置到 fields_convert_map 中。要记住,因为字段本身在 Model 中可能不存在,所以 value 是无值的,你只能使用 obj 或通过缺省值来传入其它的参数。 举例:
fields = ['title', {'name':'action', 'verbose_name':'操作'}]
def action(value, obj):
return '<a href="/delete/%d">删除</a>' % obj.id
fields_convert_map = {'action':action}
view = ListView(model, fields_convert_map=fields_convert_map)
采用这种方式,我们定义了一个不存在的 action 字段,它的内容是删除链接。
跳转到 View 页面¶
View页面一般是用来显示详细信息的,因此在显示 List 内容时,我们需要某种方法从 List 页面跳转到 View 页面。那么通常的办法就是选一个合适的字段,对它写一个 convert 函数, 返回一个跳转到view页面的链接即可。代码不再提供。
Ajax请求处理¶
与jquery easyui的结合¶
分页处理¶
ListView可以分页也可以不分页。缺省情况下 pagination=True
表示分页。当处于分页
情况下,用户可以传入pageno和rows_per_page来控制起始的页号和每页显示的条数。如何获
得这些信息,你需要在ListView之外进行获取。
Note
那么,为什么不将这个处理直接封装到 ListView中呢?因为随着前端使用的控件不同 可能会返回不同的分页关键字,比如有的使用 page和rows。所以你一般要在调用 ListView 之前进行转換。
查询与条件¶
在ListView中,第一个参数是Model的名字或类,那么为了返回正确的记录,你还可以传入 condition或query。其中condition对应合适查询条件,而query则对应合适的结果集。最终 的结果将由于传入这些参数而发生变化。整个查询的伪代码为:
if 传入了query:
结果集 = query
else:
结果集 = self.model.all()
if condition is not None:
结果集 = 结果集.filter(condition)
if 需要分页:
结果集 = 结果集.offset((页号-1)*每页条数).limit(每页条数)
SimpleListView¶
因为ListView是针对某个Model的,因此它也有一定的局限,比如在处理复杂的多表关联或 数据加工的结果就无能为力。所以SimpleListView是不与某个Model关联的,也因此你需要 定义一个表头,然后将其传入SimpleListView中。同时在convert中的obj参数值也将不再是 某个Model的对象,而有可能是一个dict或SQLAlchemy的ResultProxy对象。同时SimpleListView 也支持简单的select语句,但是在这种情况下表头还是要定义的。
参数说明¶
class SimpleListView(object):
def __init__(self, fields=None, query=None,
pageno=0, rows_per_page=10, id='listview_table', fields_convert_map=None,
table_class_attr='table', table_width=False, pagination=True, total_fields=None,
template_data=None, default_column_width=100, total=None, manual=False, render=None):
SimpleListView的参数和 ListView的差不多,与ListView相似的参数就不再解释了,只 强调一下与ListView不同或新増的参数:
- total
- 记录总数。与后面的manual一般联用。这是为了避免通过循环的方式得到记录总数。
- manual
- 是否手动传入记录总数。如果不是手动,则表示SimpleListView会自动对结果进行计数, 它一般是采用循环的方式,这样每次显示都要从头到尾遍历一遍,效率会很低。所以 可以在外部先统计好再传入,从而提高效率。
AddView¶
参数说明¶
class AddView(object):
success_msg = _('The information has been saved successfully!')
fail_msg = _('There are somethings wrong.')
builds_args_map = {}
def __init__(self, model, ok_url=None, ok_template=None, form=None,
success_msg=None, fail_msg=None, use_flash=True,
data=None, default_data=None, fields=None, form_cls=None, form_args=None,
static_fields=None, hidden_fields=None, pre_save=None, post_save=None,
post_created_form=None, layout=None, file_replace=True, template_data=None,
success_data=None, meta='AddForm', get_form_field=None, post_fail=None,
types_convert_map=None, fields_convert_map=None, json_func=None,
file_convert=True):
- model
- 此AddView所要处理的Model类或名称
- ok_url
成功后转換的URL地址。注意,它可以是一个回调函数,形式为:
def get_url(id): return '<a href="/view/%d">查看</a>' % id
为什么需要使用回调。因为它是基于这样的处理:在添加完记录后,需要跳转到view页 面。但是在调用AddView时,因为相应的对象还没有创建,所以没有对应的id,这样就 没有办法在调用时就传入还不存在的URL。因此采用回调的方式,会将保存后的id传入 回调函数,这样就可以动态创建新对象的URL地址了。如果不是跳转到view页面,则可 以考虑不采用回调。
- ok_template
- 如果用户没有定义ok_template,并且不是json的返回方式,则将使用这个参数定义的 模板来展示页面。
- form
- 对应的form对象。在缺省情况下,用户不需要传入Form相关的参数,AddView会自动根 据model、fields或meta参数来自动生成一个Form对象。但是在某些特殊的情况下,也 可以将一个生成好的form对象传给AddView,这样AddView就不会自动创建Form对象了。
- form_cls
- form是对应Form的对象。而form_cls是对应的Form类本身。AddView会自动使用form_cls 来创建form对象。使用form_cls的主要作用是定义校验处理,详情见下面的[数据校验处理]。
- form_args
此参数将在生成Form实例时传入。它是一个dict,主要可以使用的参数如:
{'action':提交对应的url, 'method':提交方法,缺省为POST, 'html_attrs':创建<form>时将使用的HTML的样式, #它也是一个dict,可以使用 {'id':Form的id值, 'class':类名} 等 'buttons':对应的按钮 }
- static_fields
- 标识哪些是静态字段。有时我们定义在fiells或AddForm中的字段并不都是需要编辑的, 而是只读的字段,通过这个参数可以指定哪些是只读字段。不过要注意的是,这些字段 在用户提交后不会在提交数据中存在。
- hidden_fields
- 隐藏字段。指定的字段将生成为
<input type="hidden" name="field_name" value="xxx"></input>
- success_msg
- 成功后的提示信息。这里AddView会自动调用flash函数。在uliweb中缺省提供了一个 uliweb.contrib.flashmessage的app,你需要把它加入到settings.ini中的INSTALLED_APPS中去。 flash的工作原理是通过session来保存下一个页面要显示的内容。所以在返回结果或跳 转到新页面时,新的页面或模板需要对session中的flash的信息进行处理。如果你使用 plugs项目,它有一个 ui.jquery.pnofity 的app是flashmessage的jquery的版本,可以 通过js的方式显示一个弹出窗口来展示,效果要好于flashmessage。因为flashmessage 是静态信息。
- fail_msg
- 出错后的提示消息。
- use_flash
- 是否信息提示采用flash方式,缺省为True。如果为Flash,则不会使用flash函数来显示 提示信息。
- data
- 传入到Form对象中的数据,它将作为初始值传入。如果用户提交后出错,则只会显示 用户提交的数据。data只是在第一次显示时生效。它是一个dict,key就是对应的字段 名。value为对应的字段类型的值。
- default_data
- 在保存数据到Model中时,如果用户没有输入值,则使用default_data中的数据,它作 为相应字段的缺省值。与data的区别:data是作为Form的初始值,default_data作为 Model的初始值。
- fields
- 可添加字段的列表。一个Model中可能有很多字段,但不是所有字段都需要在添加时录 入数据,因此可以通过fields来传入可编辑的字段列表。它也支持添加不存在的字段。 如果存在,则还需要提供get_form_field回调函数,详情见[处理不存在字段]的说明。 fields的处理和ListView的类似,它是一种动态的处理方式。如果是相对静态,可以 直接在Model中定义一个 AddForm 的class,在其中定义 fields。如果不想用AddForm 的名字,那么可以通过传入meta参数来改变。
- get_form_field
- 如果在fields或AddForm中给出Model中不存在的字段时,AddView会自动调用这个回调 函数来获得想要的字段对象。具体描述参见下面的[处理不存在的字段]。
- pre_save
在保存前要执行的回调函数,它的定义为:
def pre_save(data): ...
其中,data是一个dict,并且它将直接会传入到AddView所关联的Model中,所以你可以 在这里通过修改data的值或添加新的值,从而影响保存到Model的数据。因此可以在这里 来设置缺省值,或对数据进行进一步加工。
- post_save
在保存后要执行的回调函数,它的定义为:
def post_save(obj, data): """ obj 为保存后创建的对象 data 为保存时使用的data数据 """
如果在保存完某个对象后,还要进行其它的Model的操作,那么在post_save中是合适的 位置。
- post_created_form
在创建完Form实例后将要调用的回调函数。它允许你对生成的Form作进一步的加工,比 如将原来非必输项的某个字段的required属性改为True,从而变成必输项。它的定义为:
def post_created_form(fcls, model): """ fcls 是对应的Form类 model 是对应的Model类 """
- layout
- uliweb中的Form支持不同的布局处理。一个布局是用来处理Form展示的类,它可以决 定是使用table还是div来展示一个form。具体layout的用途和对应的layout_cls有关。 详情参见[Form的布局处理]
- file_replace
- AddForm可以支持在上传Form数据时同时上传文件。这个参数用来控制,如果出现同名
文件时,是否要替換重名的文件。现在Uliweb在上传时,可以控制是不是要对文件名
进行特殊处理,比如使用UUID来生成文件名。这样其实是不会重名的。但是如果不进
行特殊处理是有可能重名。如果重名,并且不进行替換,那么文件名会自动在后面添
加
(n)
这样的信息。 - file_convert
- 是否对上传的文件名进行转換,如果不转換则将保留原来的文件名。同时结合上面的
file_replace
将会对重名文件进行特殊的处理。 - template_data
- 将同时传入模板中的其它的数据。
- success_data
此参数可以有几个值,它是与返回json数据有关。如果在执行run()时传入了 run(json_result=True) 则表示返回结果为一个json的数据。这时,如果成功则会根据success_data的值来决定 返回的json内容。
- True
- 表示使用缺省的结果返回,那么它会简单的调用创建对象的to_dict()方法生成一个 dict,然后返回。
- function
如果要自已加工,则可以传入一个回调函数,形式为:
def success_data(obj, data): """ obj为新创建的对象 data为保存时使用的数据 """
这个函数需要返回一个dict值。
- json_func
当返回结果为json是,一般情况下会使用uliweb的json函数。但是有些情况,如在ie中使用 了iframe处理方式来调用jquery的jquery.form插件时,会有问题,原因是json返回的content_type 不正确。这里不能简单地返回
application/json
的类型,而是要返回text/html
类型,示例代码如:json_func=partial(json, content_type='text/html;charset=utf-8')
- meta
- 静态字段集定义所对应的class名。
- post_fail
- 上传数据校验失败后的回调函数处理。
- types_convert_map
- 类型转換映射。
- fields_convert_map
- 字段转換映射。它与上面的types_convert_map都是用来对静态字段进行转換处理的。 关于字段转換,详情参见ListView中的[字段转換]说明。
简单代码示例¶
def add(self):
from uliweb.utils.generic import AddView
def get_url(id):
return url_for(BlogView.view, id=id)
view = AddView(self.model, ok_url=get_url)
return view.run()
这是一段View的代码。它创建了一个AddView,而是定义了一个get_url函数用以响应保存 成功后的URL跳转。
对应的模板为:
{{extend "BlogView/layout.html"}}
{{block content}}
<h2>添加</h2>
{{<< form}}
{{end}}
View中会返回一个form对象,它就是用来接受用户输入的表格。可以直接在模板中通过
{{<<form}}
来显示出来。
执行流程描述¶
在处理完列表展示之后,我们一般要做的第一件事就是添加记录。在添加记录前应该先有一 个入口,我们一般会放在 List 的页面中。作为一个标准的 HTML 的页面编辑的处理,先 考虑采用以下的处理流程:
from uliweb import request
self.form = self.make_form(form) #创建form
if request.method == 'POST': #如果是POST则表示用户进行了提交
flag = self.form.validate(request.values, request.files) #对数据进行校验
if flag: #返回True,表示校验成功
d = self.default_data.copy() #对缺省值进行拷贝
d.update(self.form.data) #与提交的数据进行合并
if self.pre_save: #处理pre_save回调
self.pre_save(d)
r = self.process_files(d) #处理文件
obj = self.model(**data) #保存Model对象
obj.save()
if self.post_save: #处理post_save回调
self.post_save(obj, d)
if json_result: #如果需要json数据,则进行json化处理
return to_json_result(True, self.success_msg,
self.on_success_data(obj, d), json_func=self.json_func)
else:
flash = functions.flash #如果是普通的HTML方式,则获得flash函数
flash(self.success_msg) #显示成功信息
if self.ok_url: #如果指定了ok_url则进行跳转
return redirect(get_url(self.ok_url, id=obj.id))
else: #否则根据传入的模板进行处理
response.template = self.ok_template
return d
else: #返回False,表示校验失败,进行出错处理
d = self.template_data.copy() #拷贝模板数据
data = self.prepare_static_data(self.form.data) #准备静态数据
self.form.bind(data) #将数据与Form进行绑定,作为初始值
d.update({'form':self.form}) #将form对象放入模板数据中
if self.post_fail: #处理post_fail回调函数
self.post_fail(d)
if json_result: #如果需要json数据,则进行json化处理
return to_json_result(False, self.fail_msg,
self.form.errors, json_func=self.json_func)
else:
flash = functions.flash
flash(self.fail_msg, 'error')#显示出错信息
return d
else: #显示编辑页面
data = self.prepare_static_data(self.form.data) #对静态数据进行处理
self.form.bind(data) #将数据与Form进行绑定,作为初始值
return self.display(json_result)#展示页面
从上面的流程我们大概可以了解整个AddView所做的处理。上面并不是真正的代码,不过已 经和真正的代码非常接近。简单描述起来,一个添加或编辑处理大概分三个步骤:
- 如果是 GET 请求,则显示编辑界面
- 如果是 POST 请求,则对数据进行校验,如果成功则保存,返回结果
- 如果失败,则返回出错结果
上面的代码之所以看上去复杂,是因为要支持用户的扩展,所以在许多地方都添加了回调 和参数,允许用户对执行过程进行扩展。用户可以根据需要传入不同的回调来进行特殊的 处理。除了采用回调方式外,用户也可以对AddView类进行继承。
录入字段的配置¶
前面说到,AddView支持通过fields参数来设定哪些字段可以编辑,也可以支持在Model中 定义一个AddForm的class,示例如下:
class Blog(Model):
__verbose_name__ = 'Blog'
#author = Reference('user', verbose_name='作者', required=True)
create_date = Field(datetime.datetime, verbose_name='发表时间', auto_now_add=True)
title = Field(str, max_length=255, verbose_name='标题', required=True)
content = Field(TEXT, verbose_name='内容', required=True)
deleted = Field(bool, verbose_name='删除标志')
class AddForm:
fields = ['title', 'content']
这样,在AddForm中我们只定义了两个可录入的字段,其它的字段,要么使用缺省值,要么 可以自动生成,要么是在特殊情况下使用的。
处理不存在的字段¶
如果在添加时希望有一些不在Model中的字段,可以先在fields或AddForm中定义这个字段名, 然后在写一个get_form_field的回调,再将其传入AddView中即可,示例如下:
def get_form_field(name):
#其中name为对应的字段名
from uliweb.form import StringField
if name == 'undefined': #这里只是以'undefined'为例,实际可能叫别的
return StringField('不存在的字段')
fields = ['title', 'content', 'undefeined']
view = AddView('blog', ok_url=get_url, fields=fields,
get_form_field=get_form_field)
return view.run()
上面是通过动态传入fields参数来添加不存在的字段,也可以在Model中的AddForm中定义。
数据校验处理¶
从AddView的功能,我们大概可以了解到它会自动将Model转为一个Form,并且会有一些简单 的校验。如果在Field定义时我们指定了required=True,则这个字段在Form中将成为必输 项,如果用户不输入内容或输入为空的内容,则校验会失败。除了必输项,我们有可能需要 对某个字段或某几个字段进行校验,该如何操作。这里其实就直接使用了Form类本身的校验 功能。Form的校验分为两种,一种是单个字段的校验,一种是多个字段的联合校验。示例 代码如下:
class RegisterForm(Form):
form_buttons = Submit(value=_('Register'), _class="button")
form_title = _('Register')
username = StringField(label=_('Username'), required=True)
password = PasswordField(label=_('Password'), required=True)
password1 = PasswordField(label=_('Password again'), required=True)
next = HiddenField()
def validate_username(self, data):
from uliweb.orm import get_model
User = get_model('user')
user = User.get(User.c.username==data)
if user:
return _('User "%s" is already existed!') % data
def form_validate(self, all_data):
if all_data.password != all_data.password1:
return {'password1' : _('Passwords are not match.')}
上面是一个用户注册的Form,它要对用户名进行校验,还要对两次输入的密码进行校验。 对于用户名的校验采用了定义一个validate_fieldname的方式,其中fieldname是Form 中的字段。另一种方法是定义form_validate,它可以传入所有数据all_data,这样可以 同时检查多个字段。而validate_fieldname方法,只传入指定的字段值,所以无法同时检 查其它字段的值。如果有错误,对于validate_fieldname则只要返回一行出错原因的文本 即可。而form_validate则要返回一个出错的dict。其中key是出错的字段名。如果返回 None,则认为无错。在简单的情况下,你可以只写一个form_validate即可,所有的校验 都放在这里面处理。
这里的Form只是一个示例,在一般使用AddView或EditView时,你并不需要在Form中定义 任何Field。如果定义的话,它会和Model中的字段同时展示出来。
Form的布局处理¶
其它说明事项¶
URL定义规范¶
为了处理的一致性,我们一般可以假设CRUD的功能采用以下的URL定义规则,假设我们采用 class-based View的写法,如:
#coding=utf8
from uliweb import expose
from uliweb.orm import get_model
@expose('/blog')
class BlogView(object):
def __init__(self):
self.model = get_model('blog')
@expose('')
def list(self):
def add(self):
def edit(self, id):
def view(self, id):
def delete(self, id):
整个View有一个前缀,所以后面的方法都是以这个前缀为基础,你可以根据需要调整路径, 每个功能对应的 URL 为:
- list
/prefix
这里因为 list 对应的URL和前缀是一样的,所以我们使用expose('')
生成 和前缀一样的 URL。- add
/prefix/add
这里直接使用class-based的缺省函数映射方式,即: 前缀+’/’+方法- edit
/prefix/edit/<id>
方法同上- view
/prefix/view/<id>
方法同上- delete
/prefix/delete/<id>
方法同上
如果你相把 <id> 放在动作前面,那么你要在每个方法前使用如: @expose('<id>/edit')
这样的方式。如果这个view函数还有其它的decorator,那么你要把 @expose
放在前上面,
以保证函数名是正确的。同时其它的 decorator 在处理时一定要保证生成的新的函数名与
原来的函数名是一致的。
如何测试¶
根据测试的要求,我们可以将其分为:函数测试,web测试。其中,函数测试大多数情况 下可以使用象doctest的技术来实现,这里不描述了。主要讲web测试。web测试一般需要 一个环境,如web server。然后通过在客户端录制脚本来摸拟页面操作,再比较返回的 内容。因为uliweb底层使用werkzeug模块,它提供了 werkzeug.test 功能,所以你可以 使用它来进行测试。它可以摸拟web server的工作方式,通过程序的方式发出get, post 请求,还可以自动处理cookie和redirect,所以使用很方便。
为了简化在uliweb中的使用,uliweb.utils.test中提供了client的函数,它将返回一个 Client对象。关于如何使用werkzeug进行测试的文档请参见werkzeug的 文档 。
示例如下:
from uliweb.utils.test import client
c = client('..')
r = c.post('/login', data={'username':'username', 'password':'password'}, follow_redirects=True)
r = c.get('/')
print r.data
上面的代码摸拟用户登录的例子。
client接受一个project_path的参数,它是你的uliweb项目的目录,其下应该有apps子目录。 它会自动创建app。c.get()和c.post()分别对应GET和POST的HTTP的请求,返回值为Response 对象。
日志处理说明¶
logging模块使用分析¶
正确使用日志处理的前提是先要对logging模块有一个清楚的认识,为此我专门写了关于 logging使用的博文,因为和uliweb本身没有直接的关系,因此这里只放一个 链接 。
uliweb中日志配置¶
目前uliweb在default_settings.ini中已经有如下配置:
[LOG]
#level, filename, filemode, datefmt, format can be used in logging.basicConfig
level = 'info'
#filename = None
#filemode = 'a'
#datefmt = None
format = "[%(levelname)s %(name)s %(asctime)-15s] %(message)s"
[LOG.Loggers]
#logger parameters example
#{'propagate':0, 'format':'format_full', 'level':'info', 'handlers':['Full']}
#Note:format and handlers can't be existed at the same time
#if they are existed at the same time, only handlers will be processed
werkzeug = {'propagate':0, 'format':'format_simple'}
uliweb.app = {'propagate':0, 'format':'format_full'}
uliweb.console = {'propagate':0, 'format':'format_simple'}
[LOG.Handlers]
#handler parameters example
#{'format':'format_full', 'level':'info', 'class':'logging.StreamHandler', 'arguments':()}
Full = {'format':'format_full'}
Simple = {'format':'format_simple'}
Package = {'format':'format_package'}
#defines all log fomatters
[LOG.Formatters]
format_full = "[%(levelname)s %(name)s %(asctime)-15s %(filename)s,%(lineno)d] %(message)s"
format_simple = "[%(levelname)s] %(message)s"
format_package = "[%(levelname)s %(name)] %(message)s"
看上去有些复杂,让我们先做一个整体介绍,然后再区分几种常见的使用模式来讲如何配置。
在uliweb中日志可以分为:全局配置、root logger配置、其它logger配置。同时为了配置上 可以复用,handler和formatter可以单独配置,这样在logger中配置时只要引用相当的名字 就可以了。
basicConfig¶
[LOG]中定义的是全局配置,它对应于使用logging.basicConfig()的方式。因此你可以在 [LOG]中定义如下参数:
- level
- 日志级别,如:’info’, ‘debug’, ‘error’, ‘warn’, ‘notset’(‘noset’表示未设置)。 缺省是NOTSET。
- filename
- 文件名。当你需要将日志记录到文件中时使用。缺省是使用标准输出。
- filemode
- 写入文件时的模式。需要先设置filename。缺省为’a’(追加方式),可以设置为’w’(表 示写入方式,会覆盖原日志文件)。
- datefmt
- 日期格式,按datetime格式串的要求进行配置,缺省为’yyyy-mm-dd hh:mm:ss,ms’。
- format
- 日志输出格式串。详见 Python的日志记录属性 。
Note
从logging中定义的日志级别可以看到有:CRITICAL(同FATAL), ERROR, WARNING(同WARN), INFO, DEBUG, NOTSET。级别的大小是从高向低排的,最高的数值越大。当你设置了 某个日志级别,只有大于等于这个级别的才可以输出。一般来说,在创建logger或 handler时,如果没有指定日志级别,缺省都是NOTSET,所以所有的日志都会输出。
logger定义¶
针对不同的logger,可以定义不同的日志配置。所有logger都定义在 [LOG.Loggers] 中。 它可以定义logger的level, 还可以定义多个handler,在缺省情况下,当不定义handler时 会使用logging.StreamHandler类来处理。每个logger的配置形如:
[LOG.Loggers]
key = value
其中key就是logger的名字,如上面的’uliweb.app’, ‘uliweb.console’, ‘werkzeug’等。 如果logger的名字为 ‘ROOT’,则表示root logger。而root logger就是执行basicConfig() 后的日志对象。可能你要问:为什么还要可以单独处理root logger呢?因为 [LOG] 无法 定义新的handler,它要么使用文件要么使用stream。因此通过单独配置root logger,可以 定义新的handler。
value是一个字典,可以使用的参数说明为:
- propagate
- 传播标志。缺省为1,如果不传播,则要设置为0。关于传播在开始提供的博文中有描述。 主要是因为logging中的日志是可以分级的,在存在分级的情况下,当前logger处理完 一条日志后,如果传播标志为1,则一旦存在父日志对象,则会自动调用父日志handler 来输出日志。因此,你要根据你的实际配置来决定要如何设置传播。否则有可能出现 一条日志会被输出多次的情况。
- level
- logger的日志级别。
- handlers
- 处理句柄,它是一个list。它与另一个参数’format’不能同时使用。而这里处理句柄 只是一个名字的引用,如:[‘handler1’, ‘handler2’]。真正的处理句柄将在[LOG.Handlers] 中定义。
- format
- 日志输出格式串。不能与handlers连用。如果同时定义了handlers,则此项不生效。 当定义了format时,由handlers中会自动创建一个缺省的StreamHandler的句柄,其 格式串为format的值。这里格式串有两种处理方式,一种是它定义为后面[LOG.Formatters] 的一个名字的引用。另一种就是当在[LOG.Formatters]找不到时,则认为是一个普通 的格式串进行处理。
Note
为什么handlers和format不能同时定义?因为一个logger可以支持多个handler,而 format只定义了某个handler的格式串。在通常情况下,在定义handler时,同时可以 定义它的日志级别和format信息。所以format的参数在这里,只是用来处理最简单的 情况。
Note
设置什么参数会定义handler呢?一是设置了format,二是设置了handlers。其它情况 下不会创建handler。因此,你要根据一个logger是否真生有handler来考虑如何设置 propagate。如果一个logger没有handler,则没必要关闭propagate,这样就可以使用 父logger的handler来输出了。如果一个handler都没有找到,则logging会错说日志对 象还没有配置。
handler的定义¶
所有的handler都定义在 [LOG.Handlers] 中,形式为:
[LOG.Handlers]
key = value
其中key为handler的名字。
value为handler要使用的参数,可以为:
- class
- handler所对应的类对象。缺省为 ‘logging.StreamHandler’ 。注意,这里加上了 模块的路径,以便可以方便导入。
- arguments
- 需要传入handler类进行初始化的参数,缺省为 () 。
- level
- handler的日志级别。缺省为NOTSET。
- format
- 当前handler使用的日志输出格式。它有两种定义方式,一种是和后面的[LOG.Formatters] 中的formatter对应,只是一个名字。另一种是当找不到一个名字时,会自动认为是格 式串。所以简单情况下,可以直接在handler中定义format串,而不是先在[LOG.Formatters] 中先定义好formatter,然后再引用它的名字。
Note
从上面可以看出,handler和logger都可以定义自已的日志级别。同时root logger用于 定义缺省的日志级别。所你你可以根据需要在不同的对象上实现有区别的日志级别定义。
formatter的定义¶
从前面可以看出,在定义logger和handler时都可以直接定义format串,并不一定需要定义 formatter。那么formatter的存在只是为了复用。你可以先定义几种常用的日志格式,然后 在定义logger和handler时引用它,这样会比较简单。只不过要注意,在[LOG]中定义的format 不能是formatter的引用,因为它是要使用basicConfig()来处理的,而它是不接受一个 Formatter对象的。formatter的定义形式为:
key = value
共中key为formatter的名字。
value为日志的格式串。具体定义参见 Python的日志记录属性 。
应用介绍¶
配置¶
在最简单的情况下,我们可以使用缺省的定义。这样你会得到:
[LOG]
level = 'info'
format = "[%(levelname)s %(name)s %(asctime)-15s] %(message)s"
[LOG.Loggers]
werkzeug = {'propagate':0, 'format':'format_simple'}
uliweb.app = {'propagate':0, 'format':'format_full'}
uliweb.console = {'propagate':0, 'format':'format_simple'}
全局的root logger的日志级别为INFO。同时还定义了三个其它的logger: werkzeug, uliweb.app, uliweb.console。它们都有自已的日志格式。其中uliweb.app的日志会比较详细,werkzeug 和uliweb.console比较简单,就是 [%(levelname)s] %(message)s 。因为werkzeug和 uliweb.console主要日志输出是在命令行,所以比较简单。而uliweb.app则定位在应用处理 所以略复杂一些。而因为werkzeug是uliweb的底层包,它使用了 werkzeug 的日志名字, 所以单独对它进行了定义。这里因为werkzeug, uliweb.app, uliweb.console都定义了自 已的format,所以会生成相应的handler,为了避免由于传播带来的日志会输出两次,因此 设定了progagate为0。以上都是uliweb缺省设置好的,你可以直接使用,或根据需要定义 或重定义某些logger。
定义自已的logger,主要是在[LOG.Loggers]中添加新的logger的入口,然后根据需要创建 [LOG.Handlers]和[LOG.Formatters]。
使用¶
使用简单的日志可以直接使用root logger,方法为:
import logging
logging.info()
可以直接调用logging模块提供的相关的api进行输出。这是最简单的情况。也可以主动获 得root logger对象,如:
import logging
mylog = logging.getLogger('')
使用某个命名logger对象,如:
import logging
mylog = logging.getLogger('uliweb.app')
如果,指定的日志名已经在settings.ini中配置了,则可以直接使用它的配置项。如果没 有配置,则全部使用缺省的,比如日志级别将是NOTSET,并且不会有handler创建(因为不 会有format和handlers的定义)。
如果你使用了其它的组件,它们需要对日志进行配置,也可以在settings.ini中设置,一 样可以生效。
在uliweb.utils.common中提供了一个全局的log对象,它是logging的别名。因此你使用它 就相当于调用root logger。
uliweb中log的初始化¶
在uliweb中,log的初始化目前是在Dispatch初始化的时候做的。因此,在命令行中使用 uliweb的log时,注意最好在Dispatch或make_application之后使用。对于view中的处理, 一般都不用考虑初始化顺序的问题。
使用建议¶
建议在你的程序中,每次要用到logger对象时,使用logging.getLogger(name)来获得一个 logger对象。
问题与技巧¶
如何设置sqlalchemy的日志,让它只显示一次¶
在处理中发现如果在创建引擎时显示日志信息时,在uliweb运行时会显示两条日志,比如
设置 DEBUG_LOG=True
。你可以这样设置:
[LOG.Loggers]
sqlalchemy = {'propagate':0}
sqlalchemy.engine = {'propagate':0}
有关sqlalchemy的logger信息可以参考 Configuring Logging 文档。
Session使用说明¶
session简介¶
什么是session?它就是用来控制会话的一种手段。因为HTTP处理是无状态的,因此需要一 种办法记住当前用户的状态,比如:是否登录,以及记录一些额外的信息。在uliweb中, session的实现是通过:cookie和后端的session存储来实现的。让我们考虑一下session 的处理过程:
用户访问一个网站,如果以前没访问过,这时cookie中没有session要的东西(如:session_id)。 后台在接收到一个请求时,如 果发现没有对应的cookie值,则会自动创建一个session对象,并自动生成一个session_id。 然后在响应时,向浏览器返回SET_COOKIE,将这个session_id保存到前端。
保存时,session对象和cookie对象的失效期要分别设置,它们可能一样,也可能不一样。
用户再次访问,如果cookie失效了,自然无对应的Session_id的值。如果没失效,则 会将其上传。后端在处理时,如果session_id没有上送,则按1的步骤来处理。如果有 值,则从session的后端装入相关的数据。如果没找到,则同样认为session失效,则按 1的session新建方式进行处理。如果找到,则要检查是否失效,如果失效,按session 新建的方式进行处理。如果没有失效,则将相关的session信息取出来。
功能说明¶
uliweb中的session目前支持几种设置模式:
- remember me 功能实现。这种模式一般支持时间比较长的session有效期。
- 浏览器生存期cookie的设置。这种模式,session还是固定的有效期,但是cookie本身 会随着浏览器关闭才会失效。但在这种情况下,如果后台Session失效,则session仍 然会重新创建。
- 普通session设置。这是最常用的一种设置。cookie和session有效期是一致的。
uliweb session结构¶
在uliweb中session功能的实现是由一系统的组件组成的,分别为:
- weto/session 模块
- 它用来完成session类,sesscion对应的cookie类,及底层的存储调用框架,并且提 供了几个预定义的session存储后端。
- uliweb.contrib.session APP
- 它用来自动根据request来初始化session类及session对应cookie的相关参数,创建 session对象及对应的session cookie对象。当应答时,对session对象进行保存,同 时向前端发送cookie信息。
- 前端处理
- 这里应由用户来实现。在plugs项目中的userman中有一个已经实现的实例。它的作用 是生成login界面,比如増加remember me的checkbox,然后在views.py中向request.session.member 设置相应的值。
session配置说明¶
在apps/settings.ini中安装session app:
[GLOBAL]
INSTALLED_APPS = [
...
'uliweb.contrib.session',
...]
在uliweb.contrib.session的settings.ini中已经预设了一些值,简单介绍一下,这些值 你都可以在apps/settings.ini中进行重定义:
[SESSION]
type = 'file'
#if set session.remember, then use remember_me_timeout timeout(second)
remember_me_timeout = 30*24*3600
#if not set session.remember, then use timeout(second)
timeout = 3600
force = False
- type
表示session存储后端的类型,目前可用的值有:
- file 文件系统
- database 数据库
- redis redis数据库
- remember_me_timeout
- 只在设置了session.remember = True时生效。它是以秒为单位计算的值。一旦session.remember 为True时,将同时将session.cookie的有效期设置为此值。
- timeout
- 适用于一般的session设置。单位为秒。
- force
- session保存时的模式。如果为True,则只要session有修改,有值就会保存。适合于 每次访问都保存,这样,失效期会向后推迟。如果为False,则只有当有修改时才保存, 因此,在用户频繁访问的情况下,如果session没有变化,有效期不会向后推迟。
[SESSION_STORAGE]
data_dir = './sessions'
针对不同的后端要设置的参数。对于不同的后端类型,需要设置的参数不同,上面为’file’ 方式的后端参数。下面根据不同类型分别列出所需要的参数:
file
- data_dir
是session文件保存的目录
- file_dir
是session数据文件保存的目录。每个session对象将保存到一个文件中。如果没 有指定,它将是data_dir + ‘/session_files’目录。
- lock_dir
是读写session文件时所使用的文件锁目录。如果没有指定,它将是data_dir + ‘/session_files_lock’。
datebase
- url
sqlalchemy数据库连接串,和ORM一致。详情可以看ORM的文档或sqlalchemy的文档。
- table_name
session的表名。缺省为’session’。
- auto_create
是否自动创建。缺省为True。
redis
- unix_socket_path
redis socket 文件名。这是使用socket方式通用时需要进行设置。
- connection_pool
使用host, port方式连接。值为一个dict,例如: {‘host’:’localhost’, ‘port’:6379}
[SESSION_COOKIE]
cookie_id = 'uliweb_session_id'
#only enabled when user not set session.cookie.expiry_time and session.remember is False
#so if the value is None, then is means browser session
timeout = None
domain = None
path = '/'
secure = None
这个配置是针对session对应的cookie来设的。大部分参数不用太关心,只有timeout。它 可以为None或一个单位为秒的超时时间。timeout缺省为None。
当session.remember=True时,这个timeout将失效,uliweb会使用SESSION下的remember_me_timeout 来替換。当session.remember=False时,如果timeout为None时,表示浏览器会话方式,即 当浏览器关闭时,cookie才会失效。注意,cookie失效并不表示后台的session对象也失效 了,但是由于cookie失效,再访问时,session仍然是失效的。如果timeout为一个整数,则 将按这个时间来设置cookie的失效时间。这个时间一般与SESSION.timeout的时间一致。
[GLOBAL]
MIDDLEWARE_CLASSES = [
'uliweb.contrib.session.middle_session.SessionMiddle',
]
这段配置将在安装了uliweb.contrib.session app之后自动生效,将添加一个session处理 的Middleware。它将会创建session和对应的cookie对象,并且在向浏览器应答时保存 session和设置cookie。
remember me的实现¶
uliweb的session已经实现了remember的后续处理,剩下的就是如何让其生效。因此对于 用户想要使用uliweb.contrib.session来实现remember me的功能主要要完成以下工作:
- 前端的界面
- view的处理
界面很简单,就是添加一个remember之类的checkbox。在views.py的代码如:
flag = form.validate(request.params)
if flag:
request.session.remember = form.rememberme.data
先对上传数据进行校验,然后根据form的rememberme的值来设置request.session.remember即可。 上面的代码是用户登录的处理,不过省略了登录相关的代码,大家可以参考使用。
session的使用¶
在安装完uliweb.contrib.session之后,在views中的request对象上会附带有一个session 对象,它是由session的middleware创建session后加上去的。用户可以直接通过request.session 来访问。使用时,session对象就是一个dict,因此你可以使用所有字典的方法,如:
request.session['a'] = 'b' #向session中添加key为'a'的值'b'
del request.session['a'] #删除key为'a'的内容
request.session.delete() #删除整个session
session中的值一般为普通数据类型,因为当保存session时,会将值进行序列化处理。缺省 是使用cPickle来处理的。
session的保存是由session middleware来完成的,用户一般不用考虑。
通过request.session.cookie可以访问每个session对应的cookie对象,你可以修改它的值 以便保存到浏览器中。
XMLRPC 使用说明¶
Uliweb通过uliweb.contrib.xmlrpc这个app来提供XMLRPC的访问需求。通过此文来讲解XMLRPC 在Uliweb中如何使用。
配置¶
向$project/apps/settings.ini中的INSTALLED_APPS中添加’uliweb.contrib.xmlrpc’。
配置完毕后,你的应用就已经有一个/XMLRPC的url可以使用了。比如可以通过:
http://localhost:8000/XMLRPC
来访问XMLRPC服务。如果你想要修改这个URL,可以在settings.ini中添加:
[EXPOSES]
/XMLRPC = 'uliweb.contrib.xmlrpc.views.xmlrpc'
在uliweb.contrib.xmlrpc的settings.ini中已经定义上述内容。
使用¶
下面我使用一个示例的方式来展示如何使用xmlrpc。
先让我们创建一个xmlrpc_test的项目和一个Hello的app,操作如下:
uliweb makeproject xmlrpc_test
cd xmlrpc_test
uliweb makeapp Hello
修改settings.ini添加uliweb.contrib.xmlrpc:
[GLOBAL]
DEBUG = True
INSTALLED_APPS = [
# 'uliweb.contrib.staticfiles',
'uliweb.contrib.xmlrpc',
'Hello',
]
然后修改Hello/views.py,修改的内容如下:
#coding=utf-8
from uliweb import function
xmlrpc = function('xmlrpc')
@xmlrpc
def hello():
return 'hello'
@xmlrpc('func')
def new_func():
return 'new_func'
@xmlrpc
class Hello(object):
def test(self, name):
return {'user':name}
@xmlrpc('name')
def new_name(self):
return 'new_name'
说明如下:
xmlrpc提供了一个同名的decorator函数,可以用来修饰普通的函数和类,以便将其转 为XMLRPC的服务。而这个decorator是定义在xmlrpc的settings.ini中的,例如:
[FUNCTIONS] xmlrpc = 'uliweb.contrib.xmlrpc.xmlrpc'
为了获取真正的函数对象,uliweb提供了一个内部函数叫function。使用它就可以从 settings.ini的FUNCTIONS中获取一个形如’module.function’的函数对象。
xmlrpc可以支持函数和类。函数和类本身没有特别要求。对于类,一般不定义__init__ 方法,如果有,也需要可以支持不带参数的调用。因为在调用类方法时,xmlrpc会自 动创建类。
xmlrpc可以支持带参数或不带参数。不带参数则会直接使用函数名作为将来调用的函数 名。如果是类,则形式为ClassName.MethodName的形式。如果带参数,则这个参数将作 为被调用的函数名,而不是原来的函数名了。所以上面的@xmlrpc(‘func’)就是给所修 饰的函数重新起了一个名字,客户端应该使用func来访问,而不是new_func。如果是一 个类方法,这个名字会与类名进行合并。所以上面的@xmlrpc(‘name’)处理后,你就应 该使用Hello.name来访问了。
如果类函数是以’_’开头的,将不会被调用。
在类方法上仍然可以使用xmlrpc,这样相当于创建了一个xmlrpc函数的别名。
Uliweb提供的xmlrpc调用还支持与views相类似的__begin__和__end__的处理。同时可 以在类上使用。有兴趣的可以自行测试。
测试¶
下面创建一个测试程序,如test_xmlrpc.py:
#coding=utf-8
from xmlrpclib import ServerProxy
server = ServerProxy("http://localhost:8000/XMLRPC")
print server.hello()
print server.func()
print server.Hello.test('limodou')
可以看到,如果是类的方式,可以使用 server.Hello.test('limodou')
很方便。
如果执行正确,运行结果为:
hello
new_func
{'user': 'limodou'}
如何编写自已的命令¶
Uliweb内置了一个命令系统,其中在uliweb/manage.py中已经内置了一些常用命令。同时
你也可以在某个app中,按照命令编写的要求来编写app相关的命令。这样一旦在settings.ini
中的 INSTALLED_APPS
里引入了这个app,这些命令就可以被 Uliweb 使用。你可以
输入 uliweb
来查看当前有哪些可用的命令。
下面就来介绍一下如何编写自已的命令。
创建commands.py文件¶
在你的app下创建一个commands.py的文件,文件结构如下:
from uliweb.core.commands import Command
from optparse import make_option
class DemoCommand(Command):
name = 'demo'
option_list = (
make_option('-d', '--demo', dest='demo', default=False, action='store_true',
help='Demo command demo.'),
)
help = ''
args = ''
check_apps_dirs = True
has_options = False
check_apps = False
def handle(self, options, global_options, *args):
print 'This is a demo of DemoCommand, you can enter: '
print
print ' uliweb help demo'
print
print 'to test this command'
print
print 'options=', options
print 'global_options=', global_options
print 'args=', args
以上是一个命令的示例,仔细解释一下。
Attention
除了手工创建上面的文件和内容,Uliweb还提供了一个命令可以方便生成代码的模板, 使用方式为:
uliweb makecmd [appname]
如果没有给出 appname
,则会在当前目录下生成一个 commands.py
文件。
如果给出 appname
,则会在指定的 appname
下创建 commands.py
文件。
命令类¶
类属性¶
每个命令都应该从Command类派生而来。这个类有几个属性可以覆盖,分别为:
- name
命令的名字。你将使用它来执行,执行方式如:
uliweb demo
- option_list
- 它是参数列表。定义形式为
optparse
所要求的格式。每个参数都使用make_option
来定义。它支持短参数和长参数。其中短参数就是类似-d
,长参数就是--demo
。dest
表示解析后的参数将使用的变量名。default
为缺省值。action
表示解析后的值如何存储,这里为store_true
,表示一旦给出参数,则将保存为True
。所以,这种方式会将参数解析为Boolean
值。那么如果要解析为字符 串怎么做,只要去掉action
即可,缺省就是字符串,这样可以在参数后面接收 参数。help
为此参数的帮助信息。 - help
- 它会额外输出当前命令的帮助信息。用于对当前命令进行解释。
- args
将用在帮助信息的显示上。它与下面的
has_options
一起用在帮助信息的显示上。 对命令的执行没有影响。如果定义has_options
为True
,则命令帮助显示 为:uliweb demo [options] <args>
如果为
False
,则命令显示为:uliweb demo <args>
- has_options
- 用于帮助信息的显示。
- check_apps_dirs
- 用来显示是否检查当前目录下存在
apps
子目录。缺省为True
。 - check_apps
- 用来检查app是否存在。如果设置为
True
,则假定传入的参数应该是 app 。这里 的参数是进行过命令行参数解析之后剩下的参数。
类方法¶
- def get_apps(self, global_options, include_apps=None)
- 返回当前项目所有app的清单。类似于uliweb中的get_apps,不过它因为使用了global_options 所以使用会更为简单
- def get_application(self, global_options)
- 根据配置信息创建一个application的实例,它会调用
make_simple_application
。 - def handle(self, options, global_options, *args)
- 用于子类继承的方法。用户自定义的命令应该覆盖这个方法。
handler方法¶
handler方法是命令类的主体,它接三个参数:
- options
- 为本命令专有的参数,它是与类中的
option_list
的定义相对应的。 - global_options
- 为命令全局参数。Uliweb的命令系统提供了缺省的全局参数,如
-h
,-v
等 参数。因此用户在定义自已的命令参数时,注意不要与全局的参数重复。 - args
- 它就是参数解析之后剩下的参数。
常用在命令中的uliweb方法¶
- extract_dirs
从指定的模块的子目录下抽取相应的目录和文件到指定目录下。
from uliweb.utils.common import extract_dirs
函数定义为:
extract_dirs(module, path, dest_path, options)
- module
- 模块名。
- path
- 模块下的目录路径。
- dest_path
- 目标目录。
- options
可使用参数。如:
verbose=global_options.verbose
- get_apps
获得当前项目下所有的app名字
from uliweb import get_apps
函数定义为:
get_apps(apps_dir, settings_file, local_settings_file)
- apps_dir
当前项目下的apps目录。使用时如:
global_options.apps_dir
- settings_file
settings.ini文件路径。使用时如:
global_options.settings
- local_settings_file
local_settings.ini文件路径,使用时如:
global_options.local_settings
常用global_options属性¶
- verbose
- 对应于
-v
参数。表示是否要冗余输出。 - apps_dir
- 项目下的
apps
子目录。 - project
- 项目目录。
- settings
- 当前项目的settings.ini文件。用户可以使用非settings.ini名字。
- local_settings
- 当前项目下的local_settings.ini文件。用户可以使用非local_settings.ini名字。
Command类¶
Command
类是所有命令的基类。大多数方法请参见 uliweb/core/commands.py
文件。
其中有 get_apps
方法,功能和uliweb中的一样,不过需要传入的参数不同,如:
- get_apps
get_apps(global_options, include_apps=None)
使用起来比uliweb下的get_apps要简单一些。
- get_application
get_application(global_options)
获得当前应用的实例,它将完成整个应用的初始化工作
Uliweb内置APP文档:
staticfiles¶
功能说明¶
用于向uliweb中提供静态文件的服务。通常来说,静态文件服务分两种场景,一种是开发 环境,一种是生产环境。而staticfiles主要是使用在开发环境,在生产环境中,一般先 通过:
uliweb exportstatic static
将所有静态文件导出到一个统一的静态目录下,然后通过web server的静态文件处理来统 一进行服务。但是在开发环境下,一般是使用开发服务器,因此,静态文件的处理需要使 用这个app来进行。
主要功能介绍:
- 向view和模板的运行环境中注入url_for_static方法,可以方便生成静态文件的URL
- 为了在开发服务器中也提供较好的性能,提供wsgi_staticfiles.py,它是一个wsgi 的中间件,可以直接提供静态文件处理,而跳过其它的应用级的app的处理。相当是 一个旁路处理,会跳过如session之类的处理。
- 在生成url_for_static的时候,可以根据配置项中的STATIC_VER来自动在静态URL后 面添加 ?ver=xxx 的内容。这样,当静态文件发生变化,可以统一在settings.ini 中修改STATIC_VER的值,从而使那些直接或间接使用url_for_static()方法所生成的 URL发生变化,可以让浏览器重新从后台获取文件,避免因缓存带来的问题。
Note
间接使用url_for_static()的方法如: {{use “xxx”}}和{{link “link”}}的处理。
配置项说明¶
[GLOBAL]
WSGI_MIDDLEWARES = ['wsgi_middleware_staticfiles']
STATIC_VER = None
[wsgi_middleware_staticfiles]
CLASS = 'uliweb.contrib.staticfiles.wsgi_staticfiles.StaticFilesMiddleware'
STATIC_URL = '/static/'
[STATICFILES]
STATIC_FOLDER = ''
- GLOBAL/STATIC_VER
- 为 None 或 “空” 值时,不输出。为非空值时将在静态URL后面输出 ‘?ver=${STATIC_VER}’ 的值。
- wsgi_middleware_staticfiles/STATIC_URL
- 静态URL的前缀。在Uliweb中,所有静态文件都有相同的前缀。
其它的为内部使用。
静态域名输出¶
目前在uliweb的default_settings.ini中増加了:
[DOMAINS]
default = {'domain':'', 'display':False}
static = {'domain':'', 'display':False}
它定义了不同名字的域名信息。只要在你的settings中定义成:
[DOMAINS]
static = {'domain':'http://static.com', 'diaplay':True}
这样,当使用url_for_static时,静态URL将自动添加静态域名。如果display为False,则 缺省不输出。但是如果向 url_for_static 中传入 _external=True 时,则也会输出域名 信息。
soap¶
Uliweb 为了支持soap的处理,提供了 uliweb.contrib.soap app。它依赖于以下模块:
- pysimplesoap
- 目前已经内置在uliweb/lib下了,不过这个版本做了一些改动,所以不能简单地使用 原来的包进行处理。主要改动是对SoapDispatcher类的,一方面可以在构造函数中可 以传入自定义的exception处理,另一方面是在dispatch()方法中添加了call_function 参数,以便对方法调用进行封装。
- suds
这个可以用来做为客户端的使用。不过uliweb中并没有内置它,需要自行安装。也可 以使用pysimplesoap带的client类。通过导入:
from uliweb.lib.pysimplesoap.client import SoapClient
不过目前感觉还有一些问题。
- httplib2 或 pycurl
- 用于pysimplesoap的客户端处理。
配置说明¶
使用soap服务,首先要在settings.ini中安装uliweb.contrib.soap。例如:
[GLOBAL]
INSTALLED_APPS = [
...
'uliweb.contrib.soap',
...
]
同时 uliweb.contrib.soap 的settings.ini下已经预设了一些配置项,用户可以根据需要 进行覆盖,如:
[DECORATORS]
soap = 'uliweb.contrib.soap.soap'
[EXPOSES]
soap_url = '/SOAP', 'uliweb.contrib.soap.views.soap'
[SOAP]
namespace = None
documentation = 'Uliweb Soap Service'
name = 'UliwebSoap'
prefix = 'pys'
首先它定义了一个soap的decorator,因此用户后面可以使用Decorators.soap来使用它, 后面会详细说明。
然后在EXPOSES中定义了外部访问时使用的URL,缺省为 ‘/SOAP’ 。你可以根据需要进行 重定义。
然后是soap服务相关的一些描述信息。
- namespace
- 用来指明服务内部定义的一些名字空间的说明地址,缺省为None,表示和服务的URL是 一个地址。
- documentation
- 服务的说明信息。
- name
- service的名字
- prefix
- 名字空间的前缀,缺省为 ‘pys’。
View的定义¶
服务已经安装好了,下面是定义相关的处理函数。通常你仍然可以定义在views.py中。但是 要注意,这里我们并不使用@expose来处理,而是使用@soap来处理,但是它仍然有view一样 的特性。不过,返回的内容不是HTML而是XML。
举例如下:
from uliweb import decorators
from uliweb.contrib.soap import Date, DateTime, Decimal
@decorators.soap('hello', returns={'a':str}, args={'a':str})
def hello(a):
return 'Hello:' + a
在pysimplesoap中定义了一些类型,用来描述数据类型的,uliweb已经将其导到 uliweb.contrib.soap 中,因此可以直接从这里导入。pysimplesoap采用内置类型和自定义类型相结合的方式, 比如常见的有:
int, str, float, unicode, bool, short, byte, long,
integer, decimal, dateTime, date
因此,对于服务描述使用的wsdl,采用自动生成的方式。
在上面的例子中,使用@decorators.soap来定义一个soap服务。其中soap方法接受以下参 数:
- name
- 方法名,如果没有给出,则会自动将所修饰的函数名作为方法名。同时支持类方法的 方式。
- returns
- 返回值描述。它是一个字典,可以进行嵌套描述。上例表示,返回值为一个简单 类型,tag名为’a’,值是’string’类型。缺省为None。
- args
- 输入参数描述。它也是一个字典,定义方式同returns。缺省为None。如果为None,则 表示缺省为一个参数,可以是任意类型。
- doc
- 方法描述说明。缺省为None。
因此在上例中,在soap中定义了三个参数:
name = 'hello'
returns = {'a':str}
args = {'a':str}
所以这个soap方法的名字叫 ‘hello’。
soap的下面为所修饰的函数。这里正好为hello,也可以为别的。它需要定义一个参数,名 为 ‘a’,与args中的定义要一致。
返回值直接为一个字符串。在返回时会自动对它进行封装。
类型描述说明¶
类型描述就是针对returns和args的声明使用的。它支持简单类型手复杂类型。复杂类型 一般指:数组和字典。
当只有一个返回值时,一般定义为:
{'name':type}
这里type可以是前面说过的对象,如: int, str等。如果只有一个值,一般soap函数直接 返回就可以了,不必是一个字典的形式。
如果是多个值,一般定义为:
{'name1':type1, 'name2':type2}
定义为字典的形式。返回时应按说明返回一个字典。
如果是一个数组,一般定义为:
{'name':[type]}
{'name':{'sub_name':type}}
有两种定义方式。第一种会自动转換为第二种形式。但是sub_name的名字是根据type的名 字自动生成的。所以:
{'name':[int]}
其实就是:
{'name':{'int':int}}
以上几种数据定义方式可以嵌套使用,从而形成更复杂的数据格式定义。
更复杂的一些示例¶
这个例子实现将上传的整数数组相加后返回:
@decorators.soap(returns={'a':int}, args={'a':[int]})
def add(a):
t = 0
for x in a:
t += x['int']
return t
这里a其实形式为: [{‘int’:v1}, {‘int’:v2}]
这个示例实现将上传字符串数组统一在后面添加 ‘中文’ 信息:
@decorators.soap(returns={'a':[str]}, args={'a':[str]})
def string(a):
t = []
for i, x in enumerate(a):
t.append(x['string'] + u'中文')
return t
建议使用unicode进行中文处理。返回仍是一个数组。
客户端示例¶
以下以suds作为客户端来演示如何访问soap服务。以hello为例,首先准备一个服务端代码。 在某个app中的views.py中添加如下代码:
from uliweb import decorators
from uliweb.contrib.soap import Date, DateTime, Decimal
@decorators.soap('hello', returns={'a':str}, args={'a':str})
def hello(a):
return 'Hello:' + a
然后启动服务器,等待测试。
创建一个客户端的测试文件,写入如下代码:
#coding=utf8
#import logging
#logging.basicConfig()
#logging.getLogger('suds.client').setLevel(logging.DEBUG)
from suds.client import Client
client = Client('http://localhost:8000/SOAP?wsdl')
print client
result = client.service.hello('limodou')
print 'test1:', result
前几行是用来控制日志输出的。suds提供debug状态,可以根据程序处理的中间结果。这里 我已经注释掉了,你可以根据需要来使用。
首先是创建一个Client,只要传入一个wsdl的地址。这个地址就是前面的soap_url加上 ?wsdl
。
然后我们可以打印看一下这个client是什么东西:
Suds ( https://fedorahosted.org/suds/ ) version: 0.4 GA build: R699-20100913
Service ( UliwebSoapService ) tns="http://localhost:8000/SOAP"
Prefixes (0)
Ports (1):
(UliwebSoap)
Methods (1):
hello(xs:string a, )
Types (0):
可以看到我们定义的web service的一些信息,如有什么方法,需要什么参数等。
然后通过 client.service.hello(‘limodou’) 来调用服务,结果会是:
test1: Hello:limodou
更详细的示例,可以参考 uliweb-doc/projects/soap_test 中的代码。
另,通过设置apps/settings.ini:
[LOG] level = ‘debug’
也可以看到后台在接收和发送应答时的XML信息。
auth(用户认证处理)¶
在Uliweb中提供了一个auth的app,它是专门用来进行用户认证的。一般来说,用户认证功能可以理解为:登录、注册、注销、用户识别。
- 登录
- 指用户由未登录状态,通过输入用户名、口令进入登录状态。
- 注册
- 指在系统中创建新用户的过程。
- 注销
- 用户取消注册状态的过程。
- 用户识别
- 用户在登录后,在访问页面时,识别用户信息的过程。
上面只是一般的用户认证相关的内容,更复杂一些的内容,例如:
- 记住我
- 可以保留用户登录状态一段时间,比如一个月或一年等。
- 其它认证方式
- 如采用open_id认证方式,使用其它网站的认证api等。
用户认证的原理说明¶
我们知道HTTP是无状态的处理,所以为了在不同的请求中维护用户会话(session)的状态人们想出了多种办法,比如常见的session的处理。常见的session的处理基本上有两种方式:
- 基于cookie的session处理方式 这种方式也是uliweb目前采用的方式。它首先在后台生成一个session对象,每个session对象有唯一的session_id。通过session_id可以找到对应的session对象。然后将用户的id保存到session对象中。而session_id则保存到cookie中。这样,利用session本身的机制,在用户访问页面时,首先识别出session信息,如果有,则再查找有没有用户id信息,然后取出用户对象并绑定到request.user属性上,供后续的处理来使用。如果session对象没找到或没有用户id的信息,则认为用户没有登录过,并绑定request.user为None。所以,这种方式,在前端只保留session_id的信息,利用session对象来处理用户登录的状态。而且用户登录的有效期是与session一致的。
- 基于页面间传递session_id的值的方式 这种也有人用,但uliweb没有采用这种方式。它的原理是不使用cookie,而是在页面中保持一个session_id,页面间传递时都带着这个值,比如通过URL则放到QueryString中,通过POST则放到post数据中。这种不依赖cookie,但是使用起来相对麻烦。
整个认证及会话处理过程是这样的:
- 用户输入登录信息
- Uliweb在后台检查用户和口令,如果通过,则在session中保存用户id,如果不通过,则报错
- 在访问非登录页面时,根据session中的用户id信息获取用户对象,并绑定到request.user上
- 其它页面可以根据request.user为None或非None的值来知道当前访问的是登录用户还是匿名用户
用户注销的过程比较简单,只要将session中的用户id删除即可。
auth app的使用¶
Uliweb中提供了auth app来进行用户识别、登录、注销甚至注册的功能。它提供了以下功能:
- 一张用户表(User),其中还提供了:
- set_password 设置口令
- check_password 检查口令
- get_default_image_url 获得用户缺省的头像URL方法
- get_image_url 获得用户头像URL方法
check_password方法 检查两个口令是否一样的方法
- 提供了几个Form类:
- RegisterForm 注册Form
- LoginForm 登录Form
- ChangePasswordForm 修改口令Form
一个Middleware 用于用户识别
- 若干方法:
- get_user 根据request对象来获得用户对象
- create_user 创建用户
- authenticate 用户认证
- login 用户登录
- logout 用户注销
- require_login decorator,用于检查用户是否登录,如果没登录则跳转到相应的URL上,缺省为 ‘/login’ 。而且require_login既是一个decorator也是一个普通函数,主要看第一个参数是否为function对象。
- has_login 和require_login类似,但不是decorator
- 缺省view方法:
- login 用来处理用户登录,会显示登录界面和进行用户输入登录信息后的处理
- register 用来处理用户注册,会显示用户注册界面和进行用户输入注册信息后的处理
- logout 用户注销处理
- 缺省的templates
- login.html 用来显示登录界面
- register.html 用来显示注册界面
- settings.ini配置内容:
提供以下内容:
[EXPOSES] login = '/login', 'uliweb.contrib.auth.views.login' logout = '/logout', 'uliweb.contrib.auth.views.logout' [FUNCTIONS] require_login = 'uliweb.contrib.auth.require_login' [DECORATORS] require_login = 'uliweb.contrib.auth.require_login'
安装auth¶
向apps/settings.ini中的INSTALLED_APPS中添加’uliweb.contrib.auth’。
当安装完毕后,你已经可以使用/login, /logout。但是你需要在首页或某个地方将相应的链接添加进去。同时auth的中间件已经生效,它依赖于session app,用户本身还需要orm的支持。不过这两个app都已经在config.ini中写好了,会自动依赖。
用户创建¶
有了相应的链接,你需要先创建几个用户。auth本身提供了一个命令,通过:
uliweb createsuperuser
可以创建超级用户。
非超级用户,要么通过添加register相关的功能(如添加/register到页面和在views.py或settings.ini中添加相应的链接)让用户自行创建用户。要么开发相应的用户管理app来管理用户。在plugs中有类似的例子,但是已经超出auth本身的功能了。auth不提供复杂的用户管理的功能,它只是完成基本的用户认证、注销、识别等功能。
判断用户是否登录的方法¶
auth向settings.ini中注册了require_login的方法和decorator。因此,用户可以通过:
from uliweb import function
require_login = function('require_login')
if require_login():
#do something
#或
from uliweb import functions
require_login = functions.require_login
if require_login():
#do something
#或
from uliweb import decorators
@decorators.require_login
@expose('/user/admin')
def user_admin():
#do something
来使用require_login,用于判断用户是否已经登录,如果没有登录,在缺省情况下,它会自动使用名为 login 的 URL进行跳转,成功后再跳转到原来的URL上。用户可以在settings.ini中覆盖 login 的URL定义,也可以直接在require_login上传入 next=url 的参数。
auth功能扩展¶
auth虽然是一个比较基础的功能,但是在实际使用中可能有非常多的变化形式,比如使用邮箱注册,使用其它的网站进行用户认证等。这些目前还不包含在uliweb中,需要用户自行扩展。但是这里给出扩展的建议:
- uliweb提供的功能可以作为参考,用户可以基于原auth进行扩展,如:替換template,Forms, Views等
- auth提供的许多功能都是配置化的,因此用户可以考虑在自已的app中进行部分或全部替換
rbac(权限控制)¶
RBAC是基于角色控制的英文简写。在Uliweb中,提供了一个rbac的模块,它将提供一个基本的Role(角色表)和Permission(权限表),以及它们之间的关系。Role和Permission之间是多对多的关系。同时Role有指向User表的引用。
原理¶
为了保证操作的安全性,我们通常会在某些重要的操作上设置权限来控制,比如重要的链接或按钮,重要的 URL 上,前者相当于在界面上进行控制,后者相当于在服务上进行控制。在通常情况下,我们需要在展示和后台处理上都添加权限控制,以便防止用户通过程序的方式来进行攻击。
这里权限说得还是有些笼统,细分起来,我们可以通过角色或权限来控制。比如某个菜单,我们允许某个角色的用户可以访问,也可以设计成具有某种权限的角色可以访问。到底用哪个,这个取决于你的需求和设计。
在Uliweb中,角色、权限和用户之间的关系如下:
Permission m:n Role m:n User
可以看出, Permission 和 User 之间没有直接的关系。在简单情况下可以只使用角色来判断,如只区分超级用户、登录用户和匿名用户可能就足够了。因此 Permission 可能就不需要使用。在复杂情况下,使用 Permission 可能更方便,配置起来也灵活。
只使用auth app来判断基本用户角色¶
这里基本用户角色我定义为:超级用户、登录用户和匿名用户。
如果我们不使用 rbac app ,只是使用 auth app 的话,在它所提供的 User 表中已经有一些信息可以用来进行基本判断,同时结合 middle_auth ,将可以直接区分:超级用户、登录用户和匿名用户。主要分两种情况:
判断当前用户,使用request.user对象,可以区分:超级用户、登录用户和匿名用户
- 超级用户
if request.user and request.user.is_superuser
- 普通用户
if request.user and not request.user.is_superuser
- 匿名用户
if not request.user
判断指定用户,需要从 User 表中动态获取,然后利用是否存在和 is_superuser 字段来判断是否为普通用户或超级用户,这里无法判断匿名用户
- 超级用户
user = User.get(User.c.username == username) if user and user.is_superuser
- 普通用户
user = User.get(User.c.username == username) if user and not user.is_superuser
在最简单情况下,你可能只使用auth就足够了,那么我们看看rbac会带给我们什么呢?
rbac app的安装¶
首先是在settings.ini中安装app,如:
INSTALLED_APPS = [
#...
'uliweb.contrib.rbac',
#...
]
然后需要在命令行执行:
uliweb syncdb #用来创建相关的表
uliweb dbinit uliweb.contrib.rbac #初始化相应的权限数据
rbac 允许将初始的 Role, Permission 和相应的关系等写在 settings.ini 中,然后通过 dbinit 命令来进行数据装入。并且角色和权限的装入可以多次执行,它只会覆盖,不删除。
rbac的配置¶
用户可以把 rbac 相关的信息配置在 settings.ini 中,然后通过 dbinituliweb.contrib.rbac 将相关的数据导入进数据库中,主要可以使用的配置如下:
ROLES¶
ROLES用来配置角色信息,如:
[ROLES]
superuser = _('Super User'), 'uliweb.contrib.rbac.superuser', True
anonymous = _('Anonymous User'), 'uliweb.contrib.rbac.anonymous', True
trusted = _('Trusted User'), 'uliweb.contrib.rbac.trusted', True
上述是示例是由rbac/settings.ini缺省提供的,分别对应三种不同的角色:
- superuser
- 超级用户
- anonymous
- 匿名用户
- trusted
- 登录用户
ROLES每项的定义格式如:
role_name = display_name [, 'role_function_path' [, reserved_flag]]
- role_name
- 是角色的名字,应该为英文标识符。
- display_name
- 是角色对应的显示名,可以是中文
- role_function_path
- 是角色对应的判断函数路径,写法是模块路径 + 方法名。 rbac 会自动导入相关的函数进行角色的判断。这是一个可选项,也可以置为空。
- reserved_flag
- 是否保留的标志。缺省为 False ,这也是一个可选项。它的主要做用是用户可以根据它来区分当前的角色是否是保留的,从而可以进行不同的处理。 rbac 本身不对它做处理,而是将处理留给用户来使用。比如,对于保留的权限不允许删除。至于是否使用,可由用户自行决定。
关于如何判断一个用户是否某个角色,下面会详细解释,这里先不细说。
PERMISSIONS¶
PERMISSIONS用来定义权限信息,在rbac/settings.ini中没有缺省权限,示例如下:
[PERMISSIONS]
write = _('Write Permission')
它的定义很简单,就是权限名和权限名的显示文本。
ROLES_PERMISSIONS¶
ROLES_PERMISSIONS用来定义角色和权限之间的关系,如:
[ROLES_PERMISSIONS]
permission_name = role
permission_name = role1, role2, ...
permission_name = (role1, role_prop1),(role2, role_prop2)
上面是一个示例,有几种定义形式, key 为权限名, value 为角色的列表,可以是单个角色名,也可以是多个角色名,值为 tuple 或 list 。也可以是 tuple 形式的列表。如果是最后一种,则第一个元素是角色名,第二个是这个角色对应权限的附加属性。
Note
什么是附加属性?通常的权限与角色关系,我们可能只关心一个角色有什么样的权限就够了。但是对于特殊的场合,如审批处理,不同的角色可能审批的额度不同,但是对于只使用角色与权限关系的定位方式就无法定义不同的额度值来,因此在 uliweb 设计 rbac 时,在关系表中还添加了一个附加的 props 字段,利用它可以定义一些特殊的值。不过,目前没有更多对它的处理, rbac 只是把它定义成为了 PICKLE 字段,用户可以存储任意的简单数据类型,如: int, str, dict, list 等。这只是留作以后扩展使用的。
Note
在 settings.ini 中定义的上述内容,应该只是做为初始化数据时使用,在运行时不应直接使用 settings.ini 中的数据,而是通过 rbac 提供的方法或 Model 来处理。
rbac使用的表结构说明¶
Permission¶
权限表
class Permission(Model):
name = Field(str, max_length=80, required=True)
description = Field(str, max_length=255)
props = Field(PICKLE)
权限表的字段有:
- name
- 权限名称,取值应是英文标识符
- description
- 权限描述
- props
- 和前面讲的附加属性有关系。在 rbac 的设计中, Permission 中的 props 可以视为附加属性的模板和缺省值。即这个权限在关联到角色的时候,应该有哪些属性,它们的缺省值是什么。而在角色与权限的关系表中定义的是某个角色的真正取值。
Role¶
角色表
class Role(Model):
name = Field(str, max_length=80, required=True)
description = Field(str, max_length=255)
reserve = Field(bool)
users = ManyToMany('user', collection_name='user_roles')
permissions = ManyToMany('permission', through='role_perm_rel',
collection_name='perm_roles')
角色表的字段有:
- name
- 角色的名字,取值应是英文标识符
- description
- 角色的说明
- reserve
- 是否保留,留给用户使用,比如在删除时,对于保留的角色要不要有特殊处理
- users
- 当前角色所绑定的用户
- permissions
- 当前角色所绑定的权限
Role_Perm_Rel¶
角色和权限的关系表
class Role_Perm_Rel(Model):
role = Reference('role')
permission = Reference('permission')
props = Field(PICKLE)
角色和权限的关系表的字段有:
- role
- 角色id
- permission
- 权限id
- props
- 某个角色对应某个权限的附加属性
role的判断¶
对于某一个角色,rbac支持不同的判断方式,主要有:
- 通过role对应的用户来判断。即通过Role表的users字段来判断。
- 通过 role 判断方法来判断。即你可以提供一个方法,并将其按前面 ROLES 配置的说明中描述所讲的那样进行配置,这样在判断一个角色时,会使用这个方法对传入的用户进行判断。
对于 rbac 提供的缺省的 superuser, trusted, anonymous 就是采用这种方式来判断的。如果对于一个角色,两种方法都提供了,则会先使用方法进行判断,如果不满足,再按用户进行判断,直到都不满足条件。其中只要有一个满足条件,就认为用户拥有某个用户的身份。
动态角色和静态角色¶
在判断一个用户是否具有某种角色时,可能有两种情况:
一种是只根据用户信息本身就可以判断出用户是否且有某种角色,如 superuser 角色的判断,就可以根据 user 对象的 is_superuser 来判断,它不需要再依赖其它的信息。uliweb 称之为 静态角色 。
另一种情况就是,除了有用户信息外,还需要知道当前所访问的对象和用户之间的关系,如论坛的某个版块的版主,必须是和某个版块关联时才知道,只有用户信息是不够的,象这种只能在运行时,根据用户与访问对象的关系才能判断出来的角色, uliweb 称之为动态角色.
role判断函数的编写¶
写法如下:
def superuser(user):
return user and user.is_superuser
#or
def manager(user, id):
obj = Model.get(id)
return obj.user.has(user)
上面是两种写法,分别对应于静态角色和动态角色的判断。
写好之后,要按ROLES的配置要求写入settings.ini中。
通用角色、权限判断的方法¶
rbac提供两类角色和权限判断的方法,一种是通过function或functions进行函数调用的方式:
- has_role(user, *role_names, **kwargs)
- 用户要传入 user 对象和角色的名字,角色名可以是多个。同时如果存在动态角色,还要根据需要传入动态角色判断方法所需要的其它的参数。
- has_permission(user, *permission_names, **kwargs)
用户要传入 user 对象和权限的名字,权限名可以是多个。同时如果有动态角色,还要传入动态角色判断方法所需要的其它的参数。
Note
为什么判断权限有可能还需要传入动态角色所需要的参数呢?因为,在has_permission 中是根据遍历权限所对应的所有角色,检查用户是否拥有其中某个角色来处理的。
以上两个方法可以在view或模块中进行使用。简单的方法通过:
from uliweb import functions
def index():
if functions.has_role(request.user, 'superuser'):
pass
#or
from uliweb import function
def index():
if function('has_role')(request.user, 'superuser'):
pass
同时为了方便使用,rbac还提供了decorator方法,分别为:
- check_role(*roles, **args_map)
它需要传入角色名,可以是多个。其中 args_map 是一个将 view 函数中的参数映射为动态角色所需要的参数。当然它只是在有动态角色参数的时候才需要使用。并且它有一个限制就是:所要映射的参数需要在 view 函数的参数中存在。例如
def topic_manager(user, tid): """ topic_manager是用来判断一个用户是否是某个主题的管理员,它需要一个tid 的参数 """ pass from uliweb import decorators @decorators.check_role('topic_manager', tid=topic_id) @expose('/forum/topic/<topic_id>') def topic_view(topic_id): pass
上面的例子定义了一个主题管理者 (topic_manager) 的角色,它需要一个动态的角色参数 tid 。然后在 topic_view 的定义中,我们想要判断某个用户是否是某个topic 的管理员,但是 topic_view 的参数是 topic_id ,所以我们通过映射的方法将 :tipic_id 映射为 tid 。
使用 decorator 的形式,并不需要传入 user 对象,因为它会使用 request.user 。因此,它主要是用来处理 view 函数。在其它复杂的场合下,可能使用 has_role 或 has_permission 会更方便。
- check_permission(*permissions, **args_map)
- 它这decorator和check_role类似,它是用来判断当前用户是否拥有某种权限。
当使用 decorator 方法时,如果验证失败,将会自动调用 error 函数显示一个出错页面。 error 会缺省调用 error.html 模板,内容为
{{extend "layout.html"}}
{{block content}}
<div class="content">
<div class="box center col_10">
<h2>出错啦!</h2>
<div class="box-body">
<p>{{=message}}</p>
<p><a href="javascript:history.back();">点击这里回退</a></p>
</div>
</div>
</div>
{{end}}
这里主要是要有一个{{=message}}的标签。
如何将角色与用户关联¶
经过上面的学习,你可能已经了解到了如何写一个角色判断函数,并且将其配置到某个角色上。但是,如何实现用户与角色的关联呢?这个工作是由用户自已来完成的,在 plugs项目中已经有一个简单的实现,它主要是提供一个角色和权限的管理界面,并且可以关联用户和角色,角色和权限。而 rbac 只提供基本功能框架,不提供相应的管理界面。
小结¶
上面的内容有些多,下面小结一下简单的使用流程:
- 安装uliweb.contrib.rbac
- 执行uliweb syncdb创建相关的表
- 如果有自定义的角色,权限,并且希望通过命令行导入;或者角色的判断是通过函数实现的,则要在 settings.ini 中进行配置
- 执行uliweb dbinit uliweb.contrib.rbac来装入数据
- 在程序中调用 has_role, has_permission, check_role, check_permission 等方法检查用户权限
- 使用plugs/rbac_main或用户自行开发的角色、权限管理模块来管理角色或权限
template¶
Uliweb中已经可以自动处理模板了,为什么还要提供template功能?它的作用有以下几点:
- 提供了一些有关模板处理的配置项
- 为模板添加了两个新的tag:use和link
模板处理的配置项¶
打开uliweb.contrib.template的settings.ini,可以看到:
[TEMPLATE]
USE_TEMPLATE_TEMP_DIR = False
TEMPLATE_TEMP_DIR = 'tmp/templates_temp'
RAISE_USE_EXCEPTION = True
其中:
- USE_TEMPLATE_TEMP_DIR
- 表示是否使用编译缓存功能。Uliweb的模板是采用先编译成Python源代码再运行的方式。在缺省情况下,编译后的内容是放在内存中的,使用后就丢弃了。将这个选项置为True,可以将生成的Python源代码保存到一个临时目录下,如果下一次使用时,没有修改模板文件,则不再编译,直接使用缓存的文件进行执行。
- TEMPLATE_TEMP_DIR
- 与上面的选项配套使用。用来指明编译后的模板文件缓存的目录。
- RAISE_USE_EXCEPTION
- 在使用use标签时,如果找不到对应的模块是否抛出异常,缺省为抛出。
use 标签¶
在处理静态文件时,如css, js文件,比较麻烦的就是你要手工将相关的链接信息写入html中,不仅你要考虑哪些css和js要导入,它们的顺序是如何的,有可能还要考虑对于依赖其它的app的静态文件的处理,比如某个ui组件是依赖于jquery的,应如何处理和避免重复引入。因此template use设计了一种动态导入静态链接的功能,并且可以支持依赖app的静态链接导入。
use 的语法形式为:
{{use "name"}}
{{use "name", value1, var2=value2}}
use后面为要导入的模块名,并且根据需要可以添加相应的变量。可以是位置参数或关键字参数。一般来说use相关的模板都是由app的提供者写好的,用户只是按要求使用即可。
使用use的好处¶
你可以自由在模板中使用{{use}},根据use定义的次序,uliweb会自动安排返回的css, js等代码的顺序。并且会去除重复的内容。如多次使用相同的{{use}}不会有重复的内容出现。并且,它可以自动检查<head>中已经存在的链接,避免重复。
你可以主动定义链接插入的位置,有两种:toplinks和bottomlinks。它们的写法是:
<!-- toplinks -->
<!-- bottomlinks -->
你可以把它们定义在模板中的任何地方。如果没有定义,那么分两种情况:
- 存在<head>标签,则toplinks对应于<head>之后。bottomlinks对应于</head>之前。
- 不存在<head>标签,则全部显示在最前面。
use 使用说明¶
name为模块名,它需要定义在某个app的 template_plugins 目录下,这个目录需要是Python的package的写法(即目录下有一个 __init__.py),文件主名和name是一致,如,name为jquery,则文件名为jquery.py。
以jqutils.py为例,在plugs/ui/jquery/jquitls.py中:
def call(app, var, env, ajaxForm=False, hoverIntent=False, spin=False):
a = []
a.append('jqutils/jqutils.css')
if spin:
a.append('jqutils/spin.min.js')
a.append('jqutils/jqrselect.js')
a.append('jqutils/jqutils.js')
if ajaxForm:
a.append('jqutils/jquery.form.js')
if hoverIntent:
a.append('jqutils/jquery.hoverIntent.minified.js')
return {'toplinks':a, 'depends':[('jquery', {'ui':True})]}
你只要在某个.py中定义一个call函数,use标签在找到后,会自动调用。其中app, var, env是use会根据模板运行环境来提供。后面的参数是与{{use “name”,xxxx}}中定义的参数相对应的。use会自动把name后的参数传入到call函数中。
Note
因为uliweb的模板是编译后运行的,因此use中传入的参数只会在编译时生效,在运行时就无效了。所以对于运行时才生效的变量应该通过在view中传入或在模板中进行定义。
在call中,你可以修改env,这样可以添加新的变量,不过目前很少有这么做的。另一个主要的作用就是返回链接信息。这些链接信息可以是.css, .js文件,比如上面的’jqutuils/spin.min.js’。use标签会自动根据后缀是.css, .js的转为静态的链接。其它的保持不变,因此你可以使用:
a.append('<!--[if lt IE 9]>')
a.append('bootstrap/asset/html5.js')
a.append('<![endif]-->')
a.append('bootstrap/bootstrap.min.css')
传入一些代码片段。
返回时,需要是一个dict,它有两个key,一个是 ‘toplinks’, 一个是 ‘bottomlinks’。这是和前面所说的toplinks和bottomlinks的定义对应的。同时返回的dict中还可以包含depends或depends_after键字,它是用来指示外部依赖模块的。它的写法就是:
'depends':['name1', 'name2']
'depends':[('name1', {'var1':'value1'})]
有两种主要写法。第一种是列出外部依赖的use模块,全部是使用缺省参数调用,即只传入app, var, env。第二种是带参数的依赖写法,每项内容为一个tuple,第一个元素是模块名,第二个是一个dict,列出相关要传入的参数。
Note
depends用于定义所依赖的模块在本模块之前被定义。而depends_after用于定义所依赖的模块在当前模块之后被定义,例如:less.js的处理就要在.less文件之后被定义。
link 标签¶
使用use是需要有人预先写好相关的模块,它的好处是可以一次性返回多个.css, .js的信息。但是有些情况可有很简单,那么就可以考虑使用link标签,格式为:
{{link 'path/to/xxx.css', media='screen', to='toplinks'}}
其中media对应是’screen’或’print’,缺省为’screen’。to用来指示是输出到toplinks还是bottomlinks。缺省是’toplinks’。
upload(文件上传及显示处理)¶
使用werkzeug进行处理¶
在Uliweb中对上传有一定的封装处理,下面先介绍一下,不使用Uliweb提供的功能,如何使用底层的werkzeug来处理文件上传。
当用户在前端通过<input type=”file”>来上传一个文件时,在request对象中的request.files中可以得到相应的上传文件。其中是一个类dict的对象,存放多个文件对象属性,例如,<input type=”file” name=”docfile”>定义了一个docfile字段,当上传后,可以通过:
filename = request.files['docfile'].filename
file_obj = request.files['docfile'].stream
来分别处理上传的文件名和文件对象。后续你要保存到哪个目录,同时考虑到中文的问题,你可能需要将上传的unicode文件名转为与操作系统对应的本身编码。因此,为了解决这些问题,Uliweb提供了upload app来处理上传文件。同时upload app还提供上传后文件的访问处理,包括X-Sendfile的支持。
upload 的安装与配置¶
在apps/settings.ini中的INSTALLED_APPS中添加:
'uliweb.contrib.upload'
upload缺省提供以下配置:
[UPLOAD]
TO_PATH = './uploads'
BUFFER_SIZE = 4096
FILENAME_CONVERTER =
BACKEND =
#X-Sendfile type: nginx, apache
X_SENDFILE = None
#if not set, then will be set a default value that according to X_SENDFILE
#nginx will be 'X-Accel-Redirect'
#apache will be 'X-Sendfile'
X_HEADER_NAME = ''
X_FILE_PREFIX = '/files'
[EXPOSES]
file_serving = '/uploads/<path:filename>', 'uliweb.contrib.upload.file_serving'
其中:
- TO_PATH
- 为文件要保存的目录。缺省为当前路径下的uploads子目录。一般,当前路径就是你的项目目录。
- BUFFER_SIZE
- 保存文件时的块大小。
- FILENAME_CONVERTER
- 文件名转換类,它用来处理当文件上传后,在保存文件时使用的文件名。没有给出此配置时缺省是使用UUID来生成文件名,以保证文件名不重复。同时upload还定义了其它的几种转換类,可以根据需要来使用。更详细的说明参见下面具体的说明。
- BACKEND
- upload中文件上传和下载的类定义。如果没有给出,则缺省使用
FileServing
类来处理。
目前upload还支持X-Sendfile的处理方式,这是目前apache, nginx中都有的一种方法,不过细节上有所差异。关于X-Sendfile这里有一篇 文章 可以参考。简单地说就是在下载文件时可以对下载的过程进行控制,详情可参加下面的Nginx配置示例。
- X_SENDFILE
- X-Sendfile处理类型,目前只支持Nginx和Apache。根据需要可以输入’nginx’或’apache’。缺省为None,则表示不启动,则文件读取及下载是由Uliweb本身提供的。
- X_HEADER_NAME
- 当X_SENDFILE生效时,此选项用于指明将返回web server的头。目前已经知道Nginx和Apache要使用的头标识,其它的web server可以在这里指定。当保持为空时,则根据X_SENDFILE的值自动使用相应的头标识,对于Nginx则为 X-Accel-Redirect ,对于Apache则为 X-Sendfile。
- X_FILE_PREFIX
- 传给web server的头中新的URL的前缀。因为这个URL与原始的URL将不一样,所以利用这个前缀可以方便生成新的URL。这个前缀需要与web server中的内部URL相对应。不过对于Nginx和Apache的处理机制不完全相同,对于Nginx的方式,是通过返回一个新的URL,而对于Apache来说,则需要返回一个文件路径,不是一个URL。所以这个项的设置要根据所使用的web server而有所不同。对于Apache则可以认为是目录的前缀。对于Nginx则可以认为是新的URL的前缀。
- EXPOSES/file_serving
- 用于定义一个缺省的View函数,以处理上传后的文件的下载。
其实这个配置中,真正与上传有关的就是前两项。后几项都是和下载有关的。在upload中,不仅处理了上传还为了方便处理了下载。只不过,它与静态文件的下载不同,它的下载是可以在非static目录下,并且可以有view的控制参与的。而静态文件是不需要进行控制处理的。
文件上传的处理¶
其实在Uliweb有不同级别的文件上传处理,比如最原始的就是手工处理、然后就是利用upload来处理、再有就是通过generic.py来处理。在处理时,有使用Form来处理的,也可以不使用Form来处理,而是使用request,它们之间有一些差异。generic.py会专门在generic.py的文档进行讲解,这里将根据几种情景进行说明。
上传Form的定义¶
其实使用手工HTML或利用Uliweb提供的Form类来生成Form代码都没有太大关系,基本上是一样的。 简单的话,就是使用Form类了,例如:
from uliweb.form import *
class F(Form):
file = FileField(label='file')
@expose('/show_upload')
def show_upload():
form = F(action='/upload')
return {'form':form}
上面定义了一个Form类,然后我们在show_upload()中将返回一个dict,用于模板的渲染。这个view方法只处理了显示,上传还没有处理。在F创建时,我们传入action的值用于指定上传文件后的处理URL。
不使用upload app进行上传处理¶
当用户选择了文件,并提交上传后,信息将提交到/upload来。则对应的处理代码示例为:
import os
@expose('/upload')
def upload():
form = F(action='/upload')
if form.validate(request.params, request.files):
filename = request.files['file'].filename
target = os.path.join('./uploads', filename)
with open(target, 'wb') as f:
f.write(request.files['file'].stream.read())
return redirect('/ok')
else:
#指定将要使用的模板文件名
response.template = 'show_upload.html'
#如果校验失败,则再次返回Form,将带有错误信息
return {'form':form}
先生成保存目标的文件名,然后手工将上传的内容进行保存。不过,这里如果文件名有中文有可能会报错。request中得到的文件名是unicode,你需要将其转为与操作系统相匹配的编码。在Uliweb的全局配置项中提供了一个:
[GLOBAL]
FILESYSTEM_ENCODING = None
你可以考虑先对其进行配置,然后使用它来处理文件的编码。因此,你需要做的处理主要就是:
- 生成目标文件名(可能要处理文件名编码的问题)
- 保存文件
下面再看一看使用upload app的做法
使用upload app进行上传处理¶
首先安装upload app。
然后设置配置项,比如TO_PATH的值,缺省是./uploads。
将上面的代码修改一下:
import os
@expose('/upload')
def upload():
from uliweb.contrib.upload import save_file
form = F(action='/upload')
if form.validate(request.params, request.files):
save_file(form.file.data.filename, form.file.data.file)
return redirect('/ok')
else:
#指定将要使用的模板文件名
response.template = 'show_upload.html'
#如果校验失败,则再次返回Form,将带有错误信息
return {'form':form}
这里使用了upload中提供的save_file函数,它的原型为:
save_file(filename, fobj, replace=False)
这里只提供了两个参数,一个是文件名,一个是文件对象。第三个没有提供,因此如果存在 同名的文件,将不会覆盖,而是自动添加象(1), (2)这样的内容。在save_file中会自动根 据相关的配置项:文件系统编码、保存目录信息来自动生成目标文件名并转換成合适的编码, 然后保存。
为了方便处理Form字段,upload app还提供了save_file_field函数,具体使用参见下面的 函数说明。
放在一起的处理方式¶
我们可以考虑把显示和上传后的处理放在一起,也可以象这个例子一样,分开不同的URL。如果放在一起,逻辑可以是:
def upload():
from uliweb.contrib.upload import save_file
form = F()
#GET是显示用,POST是提交用
if request.method == 'GET':
return {'form':form}
else:
#如果提交,则先进行校验,这里是使用Form的方式
#form有一个validate方法,可以传入多个值,这里将request.files传入
#以便形成完整的数据集,如果validate返回True,表示校验成功,并且
#上传的数据将按照Form字段定义的类型已经做了转換
if form.validate(request.params, request.files):
save_file(form.file.data.filename, form.file.data.file)
return redirect('/ok')
else:
#如果校验失败,则再次返回Form,将带有错误信息
return {'form':form}
FileServing 类¶
upload 把文件和下载的管理组织成了类的形式。这个类就是FileServing,你可以根据需要 从这个类进行派生。在缺省情况下,upload app会自动创建一个default_fileserving,而 前面所看到的UPLOAD的配置项就是这个缺省的文件服务类。同时,基于这个缺省的实例,提 供了下面的一些方法。在简单的情况下,你可以只使用缺省的文件服务对象就够了。
FileServing的说明:
class FileServing(object):
options = {
'x_sendfile' : ('UPLOAD/X_SENDFILE', None),
'x_header_name': ('UPLOAD/X_HEADER_NAME', ''),
'x_file_prefix': ('UPLOAD/X_FILE_PREFIX', '/files'),
'to_path': ('UPLOAD/TO_PATH', './uploads'),
'buffer_size': ('UPLOAD/BUFFER_SIZE', 4096),
'_filename_converter': ('UPLOAD/FILENAME_CONVERTER', None),
}
#每个FileServing类有相应的settings配置项。因此FileServing的所有方法
#都是根据这些配置项计算来的
def filename_convert(self, filename):
"""
对文件名进行转換
"""
def get_filename(self, filename, filesystem=False, convert=False)
"""
用于获得一个文件的实际路径。它是根据to_path计算得到的。如果
filesystem为True,则会将生成的文件名按settings中配置的文件
系统编码来进行转換。convert参数用于处理是否要进行文件名的转換。
因此根据参数的不同,它有几种用法:
1. 根据传入的filename得到对应的实际路径,但文件名不转換为文件系统
的编码:
get_filename(filename)
2. 根据传入的filename得到对应的实际路径,但是文件名转換为文件系统
的编码:
get_filename(filename, filestystem=True)
3. 得到filiename的实际路径,同时进行文件名转換,这样得到的文件名将
不是原来的文件名:
get_filename(filename, convert=True)
前两种主要是用在上传文件后的显示上,这时一般使用的是转換后的文件名。
第三种是用在上传后保存文件时,先对文件名进行转換。
"""
def download(self, filename, action='download', x_filename='', real_filename='')
"""
提供下载处理,支持X-Sendfile的处理。action取值为'download'或
'inline',它们分别对应不同的应答头:
download
Content-Disposition:attachment; filename=<filename>
inline
Content-Disposition:inline; filename=<filename>
如果action为None,则不显示上面的头信息。
在这里,我们看到有三个文件名,都有什么用?
filename一般是从数据库中取出来的文件名,比如我们将文件名保存到
FileProperty中,当取出来时是Unicode格式的,并且是相对于上传路径
的相对路径,所以我们要进行转換。
如果不考虑X-Sendfile的情况,一般我们只提供filename就足够了,因
为可以自动根据to_path来计算出实际文件路径。不过当文件名并不存在
于to_path所指定的目录下时,我们还可以提供real_filename参数来指
明文件实际的路径。
对于使用了X-Sendfile的情况,又复杂了一些。我们可能还需要指出
x_filename参数,比如在nginx下,它用来指明X-Accel-Redirect中的
文件名,而这个文件路径是一个URL,提供Nginx可以找到真正的文件。
所以x_sendfile其实是一个中间路径。
所以x_sendfile和real_filename其实不会同时使用。在更底层的filedown
函数中会进行确实的处理。对于用户来说,如果想实现根据配置不同,
使用不同的下载方式,则么这些参数最好都提供。
"""
def save_file(self, filename, fobj, replace=False, convert=True)
"""
将文件保存在to_path路径下。
使用convert可以设置要不要转換文件名。
"""
def save_file_field(self, field, replace=False, filename=None, convert=True)
"""
根据文件字段来保存。路径处理同save_file
"""
def save_image_field(self, field, resize_to=None, replace=False, filename=None, convert=True)
"""
根据图片字段来保存。路径处理同save_file
"""
def delete_filename(self, filename)
"""
删除保存在to_path下的文件。
"""
def get_href(self, filename)
"""
获取filename对应的URL地址,不是真正的URL信息
"""
def get_url(self, filename, query_para=None, **url_args)
"""
获取filename对应的URL。注意,这是一个真正的URL,如果只是想得到URL的
地址,要使用get_href(filename)
如果url_args中传入了 title 和 text,则生成的URL形式为
<a href='xxx' title='title'>text</a>
如果没有传入,则使用filename代替title和text。
如果传入了query_para,则它的值将写在href对应的链接后面。query_para
是一个dict值,如: ``query_para={'alt':'filename.txt'}``
那么生成的URL可能为:
<a href='xxx?alt=filename.txt' title='title'>text</a>
它有什么用,在后面的download你会看到
"""
upload app提供方法说明¶
以下方法都是基于缺省的default_fileserving对象来处理的。
- get_backend()
- 获得缺省的文件上传下载对象。
- file_serving(filename)
缺省的文件下载函数。它是通过在 settings.ini 中配置了:
[EXPOSES] file_serving = '/uploads/<path:filename>', 'uliweb.contrib.upload.file_serving'
这样所有以
/uploads
开头的 URL都会被file_serving
处理,从而提供服务。Note
在这里还有特殊的扩展处理。在缺省情况下,上传后的文件为了保证唯一性会自动 对文件名进行转換,具体用什么要看使用哪个文件名生成器处理的。详见下面
FilenameConverter
的有关说明。因此,当下载文件名还是使用转換后的文件 名会非常不方便。所以这里有一个扩展,就是在传入的URL上添加一个特殊的 query_string,如:xxxxxxxxx.txt?alt=中文.txt
这样alt对应的就是想另存为的文件名。这样只要
<a>
标签加上alt
信息就可以 以想要的文件名来保存。- get_filename(filename, filesystem=False, convert=False)
用于获得目标文件,即将TO_PATH与filename进行连接。同时,如果给出filesystem为 True,则将文件名转为文件系统的编码。否则返回的将是unicode。
convert=False 表示不对文件名进行转換
- save_file(filename, fobj, replace=False, convert=True)
- 用于保存一个文件。需要传入文件名和文件对象,这些都可以从request或form字段中 获得。如果replace设置为True,则表示当存在同名文件时自动覆盖,否则将自动添加 (1), (2)等内容,以保证文件不重名。save_file会把文件保存到指定的目录下,并根 据配置项进行相应的文件名编码的转換。
- save_file_field(field, replace=False, filename=None, convert=True)
- 用于处理Form中的FileField字段。将自动从FileField中获得对应的文件名和文件对象。 也可以将文件保存为filename参数指定的文件名。
- save_image_field(field, resize_to=None, replace=False, filename=None, convert=True)
- 和save_file_field类似,是用来处理ImageField(图像字段)的。不过,如果你设置了 resize_to参数的话,它还可以自动对图像进行缩放处理。
- delete_filename(filename)
- 删除上传目录下的某个文件。
- get_url(filename, query_para=None, **url_args)
获得上传目录下某个文件的URL,以便可以让浏览器进行访问。
query_para 将传入到href属性后面成为query_string.
- get_href(filename, **kwargs)
- 获取filename对应的URL地址,不是真正的URL信息
Note
如果上面的文件名使用的是相对路径,则会根据当前的FileServing对象来决定使用 什么配置信息,比如文件保存的路径。但是如果使用绝对路径,则将使用绝对路径进 行处理。
FilenameConverter类的说明¶
upload提供了几个用于文件名上传后转换的类,并且可以在settings.ini进行配置,分别说明如下:
FilenameConverter:
class FilenameConverter(object): @staticmethod def convert(filename): return filename
最基本的类,对文件名不作任何转換
UUIDFilenameConverter
使用UUID方法生成文件名。文件名将保证唯一。
MD5FilenameConverter
使用MD5算法生成文件名。具体算法:
f = md5( md5("%f%s%f%s" % (time.time(), id({}), random.random(), getpid())).hexdigest(), ).hexdigest()
BACKEND配置说明¶
upload在启动时会缺省按照 UPLOAD/BACKEND
的定义来生成缺省的fileserving类。如果
没给出,则使用FileServing。通过修改 BACKEND
,用户就可以定义自已缺省的文件上传处理类。
X-Sendfile Nginx配置说明¶
简单的处理流程可以表示为:
以上的处理可以理解为:
- 用户请求的url在后台经过处理后,由后台处理添加一个内部的头信息,头信息带有一个新的URL,并且返回内容为空,因此真正的内容将由Nginx完成,所以只要添加相应的头信息即可。同时你也可能会返回其它的头信息,如: ‘Content-Disposition’, ‘Content-Type’ 等。
- Nginx在发现 ‘X-Accel-Redirect’ 头之后会自动删除,并且根据URL的信息去对应的目录下查找相应的文件,然后返回。
- 因此用户看到的文件路径有可能和真正存放文件的路径不同。并且,允许后台处理根据需要来决定返回 ‘X-Accel-Redirect’ 还是其它的信息,从而可以控制是否真正进行文件下载。一方面可以进行下载控制,另一方面可以对后台文件进行保护。
Nginx的配置如下:
location /files {
internal;
alias /path/to/files;
}
在Nginx的conf文件中添加上面的内容,需要根据需要进行修改。其中:
- /files
- 为你将在后台处理中要重新生成的URL的前缀。
- internal
- 表示内部使用,用户将无法直接通过URL来访问这个路径。
- alias
指明/files后对应的文件信息存放的路径。这里还可以考虑使用root,它们的区别就是:
例如URL为 /files/filename,如果配置为 alias /download,则将要读取的文件应该是 /download/filename,而如果配置为 root /download,则将要读取的文件将是 /download/files/filename
启用Nginx进行文件下载处理的配置项应设置为:
[UPLOAD]
X_SENDFILE = 'nginx'