Plone Cook Book v0.0.2
本书主要面向 ArcheTypes/Plone 开发者。读者不一定必须是中高级 Plone 开发人员, 但是必须要掌握一定的 ArcheTypes 基础知识。 如果您尚未有 ArcheTypes 的开发经历, 那么直接阅读本文档会有一定的困难。但这并不妨碍您通读本文档、迅速掌握其中各个知识点, 并能在实际开发中迅速索引到本文档的相关章节。 请时刻握好您的工具, Google 以及 Plone.org 右上角的 ZCatalog 接口。事实上本文档不会面面俱到, 因为它总是假定您是一名出色的黑客。 -寻找源码, 追随源码, 模仿源码, 洞察源码, 成为别人的范例源码。
Plone Cook Book
2006 年 5 日, Version 0.0.2
适应人群 & 注意事项
本书主要面向 ArcheTypes/Plone 开发者。读者不一定必须是中高级 Plone 开发人员, 但是必须要掌握一定的 ArcheTypes 基础知识。
如果您尚未有 ArcheTypes 的开发经历, 那么直接阅读本文档会有一定的困难。但这并不妨碍您通读本文档、迅速掌握其中各个知识点, 并能在实际开发中迅速索引到本文档的相关章节。
请时刻握好您的工具, Google 以及 Plone.org 右上角的 ZCatalog 接口。事实上本文档不会面面俱到, 因为它总是假定您是一名出色的黑客。
寻找源码, 追随源码, 模仿源码, 洞察源码, 成为别人的范例源码。
ATField & ATWidget 本纪
身世
- ATField 源码文件 Products.Archetypes.Field.py
- ATWidget 源码文件 Products.Archetypes.Widget.py
- 你可以在 Plone 的官方站点上找到 更多的第三方 Field 以及 Widget。
- 你必须熟读 Plone.org 上 关于 Field 以及 Widget 的文档 (Field/Widget Reference)。
- ATField 和 ATWidget 是 ArcheTypes 的钥匙和灵魂。
ATField 常用属性
- required
- default
- searchable
- accessor, 取值函数。
- 如果没有设置该属性, ArcheTypes 会自动生成一个默认函数。
- 通常默认的命名规则是 "get" + "字段名(首字母大写)"。
- 如果恰好字段是元数据, 那么默认取值函数是 "字段名(首字母大写)", 比如 "Title"。
- 可以在 ZPT 或者 Python Script 中通过 here/getXXX (ZPT) 或者 context.getXXX (Script) 调用。
- mutator, 设值函数。请参考 accessor, 不同的是设值函数的默认名称永远是 "set" + "字段名(首字母大写)"。
- validators, 验证器。
- Plone 已经提供了 isEmail、isURL、isInt 等多种验证器。具体请参见 Plone.org 上的相关文档。
- 请注意, 黑客使用 ArcheTypes 是为了避免将时间浪费在无聊的事情上, 本文档认为默认的验证器已经基本够用, 如果您发现自己总是在痛苦地编写 Validator 以适应应用, 那么无非是两种可能, 您可能搞错了, 或者您的应用搞错了。或许您需要的是一个带自定义验证的 Plone 问卷产品, 而第三方开发者早已为您准备好了。
- index, 索引类型。FieldIndex、KeywordIndex 或者 TextIndex。
- vocabulary, 可选项。您需要下载几个带下拉框的范例来理解这个字段。
- widget
ATField 关键技巧
-
对象标题总是和 Container 标题一样?
这是 Plone 的一个 Bug, 解决办法是自定义 "title" 字段 (Field) 的 accessor 和mutator。
- 当一个字段的 Widget 为 RichWidget 时, 请设置该 Field 的 default_output_type 属性的值, 如 "text/html"。否则无法使用 Plone 的编辑器 (比如 Kupu) 进行编辑。
-
中文全文索引
Plone 默认不带有中文全文索引。请首先安装润普公司开放的 ZopeChinaPak 和 CJKSplitter。
最糟糕的解决方案 (如果您的产品已经不幸进入生产系统, 这时的补救方法) 是, 手工在 ZMI - portal_catalog 中删除原有索引, 然后添加一个支持中日韩全文索引的 ZCTextIndex 索引。毫无疑问, 您需要请尽快阅读 润普公司的官方说明。
在您的项目正式开发 (事情变糟糕) 之前阅读《Archetypes 中的内容索引的设置使用》是个好主意。
CZUG 社区 当然有机会让 TextIndex 在开发时对中文全文索引透明, 为此也确有过争论。但是最后他们的决定是继续保持 Plone 在国际化上的平滑性。知道这点, 也许会让您心里暂时好过些。
-
FieldIndex:schema 的使用
在利用 portal_catalog.searchResults 进行搜索时, 它返回一堆代表搜索结果的 brain 对象, 它们是真实对象的缩写版本 (在 ZCatalog 中实现)。
得到查询结果: brains = portal_catalog.searchResults(...)
调用 brain 的 getObject() 方法可以得到真正的目标对象。通过 brain 你可以访问到该对象的元数据集 (metadata), 而对 brain 进行 getObject() 之后, 你可以访问该对象的所有属性。
得到真正的结果对象: obj = brain.getObject()
如果在您的应用中, 需要访问元数据之外的属性, brain 就不够用了, 那么你需要对搜索结果进行 getObject() 操作。不幸的是 getObject() 取得的完整版结果对象过于庞大, 在搜索结果数巨大的情况下, 您的内存将迅速报销。
如果把字段的 Index 类型设置为 FieldIndex:schema, 则可以直接通过 brain 对象来获取该字段的值。
Field:schema 做了一件很简单的事情, 它将该字段像元数据一样保存在 ZCatalog 的索引中。因此, 你可以在 brain 里面直接获取到该字段。
-
ReferenceField
该字段实际上是一个指向其他对象的指针, 用 get 方法所获取的字段值, 是其所指向的对象。
我们经常会忘记 ArcheTypes 中还有这么一个好用的工具, 实在是不应该。
ATWidget 关键技巧
- Widget 的主要属性有 label、description 以及 visible 等。
- 可以设置 "visible" 属性来让字段不可见, 或者不可更改, 如:
visible = {"view": None, "edit": None} - 在使用 SelectionWidget 时, 可以设置 Field 的 vocabulary 作为其选项。
ATContentType 本纪
身世
- Plone 预定义了 8 种 Base Class, 见 Products.Archetypes.public 中的 BaseClass 部分。
- 其中出场率最高的是 BaseContent、BaseFolder、BaseBTreeFolder, 以 Folder 结尾的, 都带有 Folder (文件夹) 的特性。
- 不用理会对 BaseBTreeFolder 性能问题的扯淡, 它能够装下几千万对象 (保守估计)。但是如果你少写了 BTree 这两个字, 您的文件夹可能只能放下几千个对象 (乐观估计)。
ATContentType 关键技巧
- 如果自定义 Folder 类型 (ContentType) 需要使用到 CMFContentPanels (润普公司开放的界面布局产品), 该 ContentType 必须同时继承 BrowserDefaultMixin。请阅读 CMFContentPanels 的具体说明, 非常地简单。
- 如果一个文件夹下面需要放相当多的文件 (大于 1000), 请从 BaseBTreeFolder 继承, 但不要忘记您是工作在对象数据库下, 所以一个目录下请尽量不要存放太多的对象。
- 忘记关系数据库在性能上超过对象数据库 (以及网状数据库) 的谣言。洞察您的应用, 区分复杂的逻辑数据 (树型结构) 和简单的平面数据, 分别存放在对象数据库和关系数据库下, 您将同时得到最高的开发效率和执行效率。
ATContentType 常见属性 (Attributes of ATContentType)
- meta_type、portal_type、content_icon、schema、allow_discussion、global_allow、all_content_types、actions、security
- 如果不想让您的产品变的无法控制, 或者仅仅是因为黑客不想了解太多 ArcheTypes 细节的时候, 请给 meta_type 和 portal_type 赋上一个相同的值。
和属性相关的关键技巧
- archetype_name 属性。设置该 ContentType 的 title。
-
_at_rename_after_creation 属性。
- 对象创建后, 系统会根据对象的 title 来改变对象的 id。
- 对于中文, 如果安装了 ZopeChinaPak, id 将自动变成 title 的中文拼音。
- 在 action 中, 重载 id 为 "view" 的视图, 可以改变默认视图。
-
设置 action 的 conditions 属性, 可以让不同权限的用户看到不同的 action 标签。比如:
'condition': 'python: object.portal_membership.getAuthenticatedMember().has_role("Manager", here)' -
去除 "共享"、"属性" 标签
#在 ContentType 中加入以下内容: class MyContentType(...):
def modify_fti(fti):
# hide unnecessary tabs (usability enhancement)
for a in fti['action']:
if a['id'] in ['metadata', 'sharing']:
a['visible'] = 0
return fti - 遗憾的是目前尚没有找到更好的方法来去除 "文件夹内容" 标签。可以仿照一个叫做 Poi 的第三方产品来做, 但会导致添加菜单的内容混乱。
ATContentType 的常用方法 (Methods of ATContentType)
-
def initializeArchetype(self, *args, **kw), 对象创建时, 将会调用该函数, 这是给开发者提供的对象初始化接口。该接口已经淘汰, 也许你会遇到使用该接口的一些旧的第三方产品, 但是在通常情况下, 请使用 at_post_create_script(self) 接口代替。
# 下面是通常的写法, 这里以 BaseFolder 为例 class MyContentType(BaseFolder):
def initializeArchetype(self, *args, **kw):
BaseFolder.initializeArchetype(self, *args, **kw)
# ... 我的代码 - def at_post_create_script(self), ATContentType 基类提供的一个 Hook (钩子), 在对象创建时会调用该函数。
- def at_post_edit_script(self), 与 at_post_create_script(self) 不同的是这个钩子是在对象被修改 (点击保存) 的时候调用的。
-
def SearchableText(self), 对象在搜索中的描述
# SearchableText 示范, 作者张炳凯
def SearchableText(self):
""" Return the information for indexing"""
return "%s %s %s" % (self.Title(),
self.Description(),
self._cookedText )
Security (Plone Permission) 世家
查看权限
- View, 在页面上显示对象, View 是必须的
- Search in catalog, 事实上这也是一个重要的查看权限, 只是我们将它通常保持在默认状态。请勿随意设置。
- List folder content, 顾名思义, 列出文件夹对象的权限。
- Access content information, 在页面上显示对象, View 是必须的。同时这个权限控制着所有对对象的操作。
写权限 (增加、修改)
- Delete Object
- Copy and Move
- Add portal content
- Modify portal content
其他权限
- Reply to item, 评注。
- Add portal member, 可以用来禁止注册功能。
关键技巧
- 请注意, 在 Plone 的默认配置下, 用户可以匿名查看站点上的资料。
- 请注意, 在 Plone 的默认配置下, 用户可以自由注册 (有 Add portal member 权限)。
- Plone 会默认添加一种名为 Authenticated 的角色。这种角色很容易造成权限配置的混乱, 建议禁止该角色。
-
设置对象的默认访问权限
security = ClassSecurityInfo()
security.declareObjectProtected('View MyContentType')
security.setPermissionDefault('View MyContentType', ('Manager', 'Owner', 'Member')) -
自动创建 (本地) 角色 (context._addRole 方法)
for role in ['MyRole1', 'MyRole2']:
self._addRole(role) -
控制对象方法的访问权限, 基本上在范例产品中, 类似 security.declarePublic('MyFunc')、security.declareProtected(permissions.View, 'MyFunc') 这种写法随处可见。但是繁复的权限控制, 通常是由糟糕并且自相矛盾的需求, 或者控制狂引起的, 我们需要时刻保持清醒以避免一切失控。尽管人类历史上从未出现过如此强大而易配置的权限系统。
Workflow 世家
创建工作流
- 安装产品 DCWorkflowDump, 进入 ZMI - portal_workflow 创建自己的工作流。点击 "Dump" 标签, 程序将自动生成该工作流的源码。
- 我们都爱 DCWorkflowDump。
protal_workflow
- State 标签页
-
Transitions 标签页, 其中 Expression 为 TALES 表达式。
python: here.getUserName() == user.getUserName()
- Scripts, 你可以让对象在状态改变时执行一个 Python Script。请参考润由 CZUG 合著的《Plone 资料汇编》第八章, 其中 "通常的任务与例子" 一节可以满足您居家旅行的大部分需要。
安装工作流
# Using permissions and workflow in your custom products
# Author, Martin Aspeli
def install(self):
from Products.MyProduct.Extensions import MyWorkflow
wf_tool = getToolByName(self, 'portal_workflow')
if not 'my_workflow' in wf_tool.objectIds():
wf_tool.manage_addWorkflow('my_workflow (My custom workflow)',
'my_workflow')
wf_tool.updateRoleMappings()
wf_tool.setChainForPortalTypes(pt_names=['MyType'],
chain='my_workflow')
关键技巧
-
获取当前对象的 state
- 对于 brain 类型的对象 (比如搜索结果), 可以直接使用 brain.review_state 获得
- 对于常规类型, 可以使用 getInfoFor 方法取得
wf_tool = getToolByName(self, 'portal_workflow')
st = wf_tool.getInfoFor(self, 'review_state')
- 获取 State 的 Title
wf_tool = getToolByName(self, 'portal_workflow')
title = wf_tool.getTitleForStateOnType(st, self.portal_type) - 获取当前对象的工作流的所有状态
def getAllStatesChainForCT(self, content_portal_type):
""" Get all the state about the portal_type """
wftool = getToolByName(self, 'portal_workflow')
chain = wftool.getChainForPortalType(content_portal_type)
workflow = getattr(wftool, chain[0])
states = getattr(workflow, 'states')
state_list = []
for id, state in states.items():
state_list.append((id, state.title))
return state_list -
执行 Transition (状态跃迁)
wf_tool = getToolByName(self, 'portal_workflow')
wf_tool.doActionFor(self, 'transition_name')
Python Scripts 列传
Request 对象
用户提交的数据会以字典形式存储在 Request 对象中。
- 在 ZPT 中, 该对象的名字是 request
- 在 Python Script 中, 该对象为 context.REQUEST
Response 对象
RESPONSE = context.REQUEST.RESPONSE
- 处理完毕后, 如果需要 redirect 到另一个页面, 则 Response 就可以派上用场。
RESPONSE.redirect(URL)
- 使用 Plone 系统的消息功能
RESPONSE.redirect(context.absolute_url() + '?portal_status_message=发生错误,请与管理员联系')
- 设置 Cookie
RESPONSE.setCookie('变量名', 变量值, expires='Web, 19 Feb 2020 14:28:00 GMT')
评注·列传
添加评注
dtool = getToolByName(context, 'portal_discussion')
tb = dtool.getDiscussionFor(context)
id = tb.createReply(title='TITLE', text='TEXT', creator='CREATOR')
reply = tb.getReply(id)
获取对象所有评注
def getRs(obj, replies, counter):
rs = pd.getDiscussionFor(obj).getReplies()
# 对评注按修改日期排序
rs = container.sort_modified_ascending(rs)
for r in rs:
replies.append({'depth':counter, 'object':r})
getRs(r, replies, counter=counter + 1)
replies = []
pd = container.portal_discussion
from Products.CMFDefault.DiscussionTool import DiscussionNotAllowed
try:
pd.getDiscussionFor(page_obj)
except DiscussionNotAllowed:
# We tried to get discussions for an object that has not only
# discussions turned off but also no discussion container.
return []
getRs(page_obj, replies, 0)
return replies
角色·列传
pm = getToolByName(self, 'portal_memebership')
创建角色
self._addRole('ROLE')
获取角色列表
pm.getCandidateLocalRoles(context)
分配角色
pm.setLocalRoles(obj=self, member_ids=[...], member_role='Contributor')
删除用户角色
pm.deleteLocalRoles(obj=self, member_ids=[...])
港台音乐
手工 (强制) 提交事务
import transaction
transaction.commit()
获取当前登录用户
- ZPT, user.getUserName()
- ArcheTypes, portal.portal_membership.getAuthenticatedMember()
在 Skin 上显示 HTML 格式的文本
<span tal:content="structure python: here.CookedBody(stx_level=2)">
不重启, Refesh Products
- 在你的 Product 目录下添加一个名为 "refresh.txt" 的文件
- 进入 ZMI:ZOPE-ROOT/control_panel/products/MyProduct, 在 Refresh 页面下可对 Product 进行 Refresh 操作, 也可以设置 Auto Refresh Mode 来进行定时 Refresh
获取请求的 URL
context.REQUEST['URL']
对 searchResults 的查询结果按照对象的 title 进行排序
context.searchResult(sort_on = 'sortable_title', ...)
在自定义 form 里面调用 Kupu 编辑器
<metal:block tal:define="
editor python: here.portal_membership.getAuthenticatedMember().wysiwyg_editor;
wysiwyg python: test(editor and (editor!='None'), editor, 'None');
use_wysiwyg python: 1 ">
<metal:block tal:define="
inputname python:'my_input';
formname python:'my_from';
inputvalue python:'the content of the field';
width python:200;
height python:300;
tabindex tabindex/next; ">
<div metal:use-macro="here/wysiwyg_support/macros/wysiwygEditorBox">
The WYSIWYG code
</div>
</metal:block>
</metal:block>

图片