cookbook/cookbook

与Web2Py共舞

(前身为Gluon) 由Massimo Di Pierro 创建 由limodou翻译

也许你已经听过说web2py,它是Web开发框架中的新成员。web2py使用Python进行编写,所 以它很可靠并且比Ruby on Rails快。web2py本身也是一个web应用,所以你可以通过浏览 器对你的应用程序进行所有的开发、部署和维护,而这种方式使得它比其它任何框架都易 于使用。除此之外,web2py被打成一个完整的包(可用于Windows, Mac或Unix/Linux),同 时包含了开发所需要的一切(包括Python, SQLite3, 和多线程web服务器). [译注: 现在 是cherrypy]

你可以从这里得到web2py: http://www.web2py.com 。 这篇文档在设计时有意模仿了 http://onlamp.com/pub/a/onlamp/2005/01/20/rails.html 。 这样你就可以同Rails进行比较了。

什么是Python?

Python是一种面向对象的编程语言,被设计得超级容易教学,并且在功能上没有任何打折。 绝大部分Java算法都可以用Python来重写,而长度仅为原来的二十分之一。Python自带了 一整套可移植的库,包括对许多标准互联网协议(http, xml, smtp, pop, 和imap, 只提到了几个)的支持和对操作系统API的支持。

什么是web2py?

web2py是使用Python编写的一个开源web框架,并可以使用Python进行数据库驱动的web 应用方面的快速编程。如今有许多的web框架,包括Ruby on Rails, Django, Pylons和 Turbo Gears,所以为什么又开发一个呢?我是在心中带着下面的目标进行web2py的开发的:

  1. 尽可能象Rails, 但是用Python来开发,这样可以更稳定和更高效。

  2. 一体化的包,不需要安装、无配置和不需要shell脚本。

  3. 超级容易教学(我的工作是教学)。所以我把web2py本身也做成了一个web应用程序。

  4. 从上到下的设计,这样web2py的API从头一天开始就是稳定的。

眼见为实

web2py编程象Rails编程一样容易,但如果你既不会Python也不会Ruby,web2py学起来要比Rails容易多了。

最重要的是,与同等功能的J2EE或PHP相比,web2py所需的代码量要少,同时它强迫你使用一种非常好并且安全的编程习惯。

web2py阻止目录遍历,SQL注入攻击(SQL injection),跨站脚本执行(cross site scripting),和回复攻击弱点(reply attack vulnerability)。

web2py替你对session,cookie和应用错误进行管理。所有应用错误都会生成一个ticket发送给用户,并且会为管理员生成一条日志项。

web2py会为你编写所有的SQL。它甚至可以创建表并决定何时执行一个数据库迁移的动作。

试一下吧。

软件安装

访问 http://mdp.cti.depaul.edu/examples 并下载Windows, Mac或Unix文件。

如果你选择使用Windows或Mac版本,你只需要执行:unzip文件,然后分别点击web2py.exe或wweb2py.app。

如果你选择使用Unix版本,你需要安装Python解释器(版本2.4或更高)和SQLite3数据库。 [译注:2.4与2.5有些区别。在2.5中使用sha512的摘要算法,而2.4只使用sha。同时2.5内置了sqlite,因此不需要安装。如果有加密数据,则2.4与2.5下的版本处理可能有不同,需要注意。] 有了这些之后,unzip web2py然后运行

python web2py.py

在生产配置中,你应该使用PostgreSQL或MySQL而不是SQLite3。从web2py的角度来看,修改配置象修改程序中的一行代码一样简单,不过在这里不讨论它,因为开发中你不需要关心它。

运行web2py

在启动时,web2py会问一个问题: “choose the administrator password”,选择一个。 在那之后,web2py会替你打开一个web浏览器(记住未曾输入过命令!),同时显示这个 欢迎页面

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image1.png

点击 “administrative interface”

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image2.png

然后输入在启动时选择的口令。你应该被重定向到管理界面的”site”页面:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image3.png

这里你可以:

  • 安装和反安装应用程序

  • 创建和设计(编程)你的应用程序

  • 清理错误日志和session

  • 以字节码方式编译应用程序用来分发和快速执行

web2py自带了三个应用程序: admin (管理界面本身), examples (交互文档), 和 welcome (可用来创建其它应用程序的基础模板)。

开始写代码

我们将创建一个在线的协作方式的 cookbook ,用来保存和分享菜谱。我们想让我们的 食谱书有以下功能:

  • 显示所有菜谱的清单。

  • 创建新菜谱和编辑存在的菜谱。

  • 给菜谱设定 category (象”dessert”(餐后甜点)或”soup”(汤))。

如果需要,你可以下载完整的web2py Cookbook例子并且跟着做。

创建一个空的web2py应用程序

为创建一个新的应用,在合适的字段 [译注:create new application] 上输入一个名字 (在本例中为cookbook),然后点击Submit按钮:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image4.png

一个新的web2py应用程序不是空的,它是welcome应用程序的克隆。它包含单个controller(控制器), 单个view(视图),基本layout(布局),通用view和称为appadmin(不要与admin混了,admin 是整个站点的管理界面)的数据库管理界面。

测试空的Web应用程序

你已经运行了web2py web服务器,所以其实没有什么可以测试的。不管怎么样,在 cookbook/design 上点击,你会看到

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image5.png

在这里你可以查看/创建/编辑你的应用程序组件。在 Controllers 下,有一个叫做 default.py 的文件,带有 “exposes index”。如果你在 index 上点击,你创建 的新应用程序会 “welcome you”

web2py Model(模型) View(视图) Controller(控制器) Design(设计)

任何web2py应用程序由以下内容组成:

  • Models: 用来描述你的应用程序的数据存储的文件。例如,你的数据库表中的字段、 它们的关系和要求。web2py会告诉你在每个model文件已经定义了哪些表。 [译注: model.tables?]

  • Controllers: 包含你的应用程序处理逻辑的文件。每个URL唯一映射到一个controller 文件的一个函数上。这个函数可以生成页面、委派一个view来渲染一个页面、重定向 到另一个URL或引发一个异常(根据异常的不同,有可能会产生一个ticket或出现在一个 HTTP错误页面中)。web2py会告诉你每个controller文件所暴露出来的函数。 [译注: 如果在controller中的函数使用``__``开始,如``def __init()``,它将是一个私有 函数,不会被暴露出来,符合Python定义的习惯。]

  • Views: 包含HTML和特殊的 {{ }} 标签的文件。这些标签可以在由controller中返回的 变量中进行渲染。这是你的应用程序的展现层。web2py会告诉你何时一个view从其它的 view中进行扩展(extend)或导入。

  • Languages: 包含所有你想要支持为其它任一语言的字符串的翻译列表的文件。这些字 符串需要你明确标识为语言依赖。 [译注:在需要翻译的字符串使用T()函数进行封装。 不过目前好象只能用在controller, view, model中。对于其它的模块好象还不支持。]

  • Static files: 其它的所有文件,包括图片、CSS、JavaScript,等等。

注意,你即不需要一个编辑器,也不需要知道web2py的目录结构,因为你可以通过design 页面来创建和编辑。 [译注:web2py自带的web编辑器可以很好的支持语法高亮,包括 Python。不过对于某些静态文件,如JavaScript不能进行修改,希望在以后可以改进。]

还要注意,为每个controller函数(在Rails中叫action)给定一个view是一种好的习惯,不 过不一定非要如此,因为当没有指定view( [译注:可以简单理解为模板] )时,web2py 会总是使用 generic.html view来渲染任何页面。

URL和Controller

这张图展示了web2py核心功能的通用结构

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image6.png

一个象

http://hostname/cookbook/default/index/bla/bla/bla?variable=value

的链接会产生一个对cookbook应用程序的 default.py controller中的 index() 函数的一个调用。

“bla”, “bla”, 和 “bla” 将被传递为 request.args[0:3] ,而”value”将被保存在 request.vars.variable 中。

controller函数应该象下面一样返回一个dict(字典)

return dict(name=value, othername=othervalue)

这样变量 nameothername 将被传到相应的view中。

现在试一试,从 cookbook/design 来创建一个 test.py controller(只需要输入名字并点击Submit),编辑 test.py 然后创建你自已的 index 函数。 [译注:如果文件名不带.py扩展名则web2py会自动添加,其它的也类似。]

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image7.png

回到 cookbook/design 并在 test.py 所暴露出来的 index 函数上点击。

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image8.png

web2py使用了 generic.html view(它是从基础的 layout.html 扩展来的)来对你的 index() 函数返回的变量进行渲染。

激动的时刻开始了...

模型创建

回到 cookbook/design ,然后创建一个叫 db.py 的新model(只需要在适当的字段上输入 db 然后点击Submit)。model定义在这里与Rails有些不同。在web2py中,一个model是一个包含了在每个数据库中 所有表 的定义文件。

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image9.png

编辑则才创建的 db.py model,然后输入下面的代码:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image10.png

这个model定义了两个表 categoryrecipe 。**recipe** 有一个 category 字段,它是一个对 db.category 的引用,并且 date 字段缺省为今天。每个字段有一些要求(这是可选的), categore.name 要求一个新值要满足 IS_NOT_IN_DB(字段必须唯一), recipe.category 要求这个字段满足 IS_IN_DB(引用是有效的), recipe.date 要求它要包含一个有效的日期。 [译注:如果你同时希望它可以为空,或不为空时需要为有效的日期格式,可以使用 IS_NULL_OR(IS_DATETIME())]

这些要求将被强制使用在任一入口的表单中,无论它是管理界面部分或用户生成的部分。 [译注:web2py会提供象SQLFORM这样的东西进行记录的录入,用户也可以使用它。而SQLFORM会对字段的约束项进行检查]

数据库管理界面(appadmin)

回到 cookbook/design ,在model下,你会看到两个新的链接,分别为 database administrationsql.log 。在前者上点击,如果不存在拼写错误时你会看到:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image11.png

这是你的数据库管理界面。试着插入一个新的category记录:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image12.png

和一些新的菜谱:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image13.png

这难道不比Rails简单吗?甚至都不需要和PHP、JSP、ASP、J2EE等相比。

是谁创建的这些表呢?web2py干的!web2py会查找一个叫做db.db的数据库( [译注:在SQLDB中定义的文件名,因为使用的是SQLite3数据库。同时使用SQLite3你还可以使用绝对路径。否则它会在你的应用程序目录下的databases子目录下创建这个数据库文件。]),如果找不到,那么它会创建这个数据库和你刚才定义的表。如果你修改了一个表的定义,web2py会替你修改表结构。如果你定义了另一张表,它会被创建。你可以看一下由web2py为这种迁移所生成的SQL,通过点击 sql.log

随便探索管理界面,插入几条记录,并试着列出它们。

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image14.png

这张表可以通过点击表头进行排序,并且当记录超过100条时会进行分页。试一下JOIN(表的联接操作),在SQL FILTER字段上输入 recipe.category=category.id

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image15.png

字段 id 是从哪来的?在web2py中的每张表都有一个唯一的整数键叫做 id 。如果你在表的 id 值上点击,你就可以单独修改这条记录。

注意 appadmin.py 是你的cookbook应用程序的一部分,所以你可以对它进行阅读和修改。在这个教程中,我们不这样做,而宁愿从头编写一个新的controller。这样会起到更好的宣传作用。

创建函数(Action)

cookbook/design 中,编辑 test.py controller 并加入如下代码:

def recipes():
    records=db().select(db.recipe.ALL,orderby=db.recipe.title)
    return dict(records=SQLTABLE(records))

现在回到design,在 recipes 上点击,你会看到

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image16.png

注意,传给view的变量 records 是一个 SQLTABLE 对象,它知道如何把自身渲染为CSS友好的HTML。变量 records 是由 generic.html view来渲染的。

让我们再改一下。修改controller为:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image17.png

注意:

  • recipes 现在返回记录列表,而不是一个SQLTABLE,而且它会从表的 category 字段生成一个选择 表单 .

  • show 接受 rquest.vars.id ,并且执行选择,一旦失败,它会重定向到 recipes

  • new_recipe 返回一个SQLFORM对象,它可以根据一个表(db.recipe)的定义创建一个HTML表单。 form.accepts() 执行对表单的校验(根据模型中的需求),用错误信息来更新表单,如果检验成功,它插入新记录到数据库中。

  • URL(r=request,f='function') 可以在当前的应用程序和controller中生成 “function” 的url,根据HTTP请求。

这块代码使用通用view已经可以完全运行,但是我们将在下面的布局层执行额外的客户化处理。

注意,一些检验器(validator),象用于’datetime’字段上的 IS_DATETIME() ,缺省是自动设置的。 [译注:如果你不想使用缺省的validator可以设置字段的 ``field.requires=[]`` ]

创建View

现在为 recipes 创建一个view。这个view叫做 test/recipes.html (在适当的字段上输入带路径的名字然后点击Submit)。

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image18.png

编辑新创建的文件

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image19.png

现在再试着调用 recipes

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image20.png

注意在 {{ }} 标签中的代码是Python代码,需要注意:

  • 不需要缩近,一个代码块以末尾为冒号的行开始,到以开始为pass的行结束(例如:def :return, if:elif:else:pass和try:except:pass)。

  • view可以看到在model中定义的所有东西加上由controller返回的变量。

  • {{=something}} 将把 something 渲染成为HTML,之后对特殊字符进行转义。

注意

{{=A(message,_href=link)}}

它是一个html辅助函数。会简单地替你输出

<a href= "link">message</a>

标签。

创建 test/show.html ,包含:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image21.png

看上去象这样:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image22.png

最后创建一个 test/new_recipe.html ,包含:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image23.png

看上去象这样:

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image24.png

注意web2py在表格中是如何把字段名首字母大写的,并且根据指定的需求为category字段生成了一个 SELECT/OPTION 字段。

如果你不喜欢 [web2py]cookbook 的广告条或它的CSS,你可以在 layout.html 文件中编辑它们。

一些魔术

如果你试图提交一个不满足需求(例如试图提交一个空的菜谱)的表单,web2py会通知你相关的提示信息。

http://fy.py3k.cn/sitemedia/p/web2py/cookbook/media/image25.png

结论

我们已经编写了一个可工作的web2py应用程序,仅仅通过浏览器,几个点击和总共53行代码。我们还得到了一个自由使用的数据库管理界面,它可以让你插入、选择、更新和删除单条记录或记录集。

web2py也包括了容易使用的函数来导入和导出CSV格式的表数据,生成RSS feed和RTF文件(与MS Word兼容),处理JSON用于AJAX。

如果想要了解更多关于web2py,请访问网页: http://mdp.cti.depaul.edu

如果你有问题,请加入Google用户组: http://groups.google.com/group/web2py?hl=en

附录 数据库API

连接到sqlite3数据库文件test.db

>>> db=SQLDB("sqlite://test.db")

或连接到MySQL数据库

>>> db=SQLDB("mysql://username:password@host:port/dbname")

或连接到PostgreSQL数据库

>>> db=SQLDB("postgres://username:password@host:port/dbname")

可用字段类型

>>> tmp=db.define_table('users',\
    SQLField('stringf','string',length=32,required=True),\
    SQLField('booleanf','boolean',default=False),\
    SQLField('passwordf','password'),\
    SQLField('textf','text'),\
    SQLField('blobf','blob'),\
    SQLField('uploadf','upload'),\
    SQLField('integerf','integer'),\
    SQLField('doublef','double'),\
    SQLField('datef','date',default=datetime.date.today()),\
    SQLField('timef','time'),\
    SQLField('datetimef','datetime'),\
    migrate='test_user.table')

一个字段就是一个SQLField类型的对象

>>> SQLField('fieldname', 'fieldtype', length=32,\
             default=None,required=False,requires=[])

删除表

>>> db.users.drop()

插入、选择、更新、删除的例子

>>> tmp=db.define_table('person',\
          SQLField('name'), \
          SQLField('birth','date'),\
          migrate='test_person.table')
>>> person_id=db.person.insert(name="Marco",birth='2005-06-22')
>>> person_id=db.person.insert(name="Massimo",birth='1971-12-21')
>>> rows=db().select(db.person.ALL)
>>> for row in rows: print row.name
     Marco
Massimo
>>> me=db(db.person.id==person_id).select()[0]
>>> me.name
'Massimo'
>>> db(db.person.name=='Massimo').update(name='massimo')
>>> db(db.person.name=='Marco').delete() # test delete

更新单个记录

>>> me.update_record(name="Max")
>>> me.name
'Max'

复杂搜索条件

>>> rows=db((db.person.name=='Max')&\
            (db.person.birth<'2003-01-01')).select()
>>> rows=db((db.person.name=='Max')| \
            (db.person.birth<'2003-01-01')).select()
>>> me=db(db.person.id==person_id).select(db.person.name)[0]
>>> me.name
'Max'
>>> rows=db(db.person.birth.month()==12).select()
>>> rows=db(db.person.birth.year()>1900).select()
>>> rows=db(db.person.birth==None).select()
>>> rows=db(db.person.birth!=None).select()
>>> rows=db(db.person.name.upper()=='MAX').select()
>>> rows=db(db.person.name.like('%ax')).select()
>>> rows=db(db.person.name.upper().like('%AX')).select()
>>> rows=db(~db.person.name.upper().like('%AX')).select()

orderby, groupby 和 limitby 的使用

>>> people=db().select(db.person.name,orderby=db.person.name)
>>> order=db.person.name|~db.person.birth
>>> people=db().select(db.person.name,orderby=order)
>>> people=db().select(db.person.name,orderby=order,\
                       groupby=db.person.name)
>>> people=db().select(db.person.name,orderby=order,limitby=(0,100))

一对多关系的例子

>>> tmp=db.define_table('dog', \
          SQLField('name'), \
          SQLField('birth','date'), \
          SQLField('owner',db.person),\
          migrate='test_dog.table')
>>> dog_id=db.dog.insert(name='Snoopy',birth=None,owner=person_id)

简单JOIN(连接)

>>> rows=db(db.dog.owner==db.person.id).select()
>>> for row in rows: print row.person.name,row.dog.name
Max Snoopy

多对多关系的例子

>>> tmp=db.define_table('author',SQLField('name'),\
                        migrate='test_author.table')
>>> tmp=db.define_table('paper',SQLField('title'),\
                        migrate='test_paper.table')
>>> tmp=db.define_table('authorship',\
        SQLField('author_id',db.author),\
        SQLField('paper_id',db.paper),\
        migrate='test_authorship.table')
>>> aid=db.author.insert(name='Massimo')
>>> pid=db.paper.insert(title='QCD')
>>> tmp=db.authorship.insert(author_id=aid,paper_id=pid)

SQLSet

>>> authored_papers=db((db.author.id==db.authorship.author_id)&\
                       (db.paper.id==db.authorship.paper_id))
>>> rows=authored_papers.select(db.author.name,db.paper.title)
>>> for row in rows: print row.author.name, row.paper.title
Massimo QCD

用belongs进行搜索

>>> set=(1,2,3)
>>> rows=db(db.paper.id.belongs(set)).select(db.paper.ALL)
>>> print rows[0].title
QCD

嵌套选择

>>> nested_select=db()._select(db.authorship.paper_id)
>>> rows=db(db.paper.id.belongs(nested_select)).select(db.paper.ALL)
>>> print rows[0].title
QCD

嵌套选择

>>> nested_select=db()._select(db.authorship.paper_id)
>>> rows=db(db.paper.id.belongs(nested_select)).select(db.paper.ALL)
>>> print rows[0].title
QCD