1. Ploneプログラミングレシピ
    1. 導入
    2. アイテムの作成とコピー
      1. コンテンツオブジェクトの作成
      2. invokeFactoryのセキュリティチェックや型チェックを迂回する方法
      3. Zopeオブジェクトの作成
      4. コンテンツオブジェクトのコピー
      5. ウェブ上でのコンテンツの作成
    3. フィールドの値の読み書き
      1. フィールドへのアクセス
      2. Field.getおよびField.setの使用法
      3. インデックスの再構築
      4. オブジェクトの型の取得
    4. フォルダの中身の一覧、ポータルルートやサイトツールの使用
      1. ポータルアイテムの格納方式
      2. フォルダの内容の一覧取得
      3. 特定の型のアイテム一覧の取得
      4. フォルダのアイテムのIDの取得
      5. 特定のオブジェクトIDが存在するかどうかの確認
      6. 獲得 (Acquisition)
      7. コンテンツの親コンテナの取得
      8. ポータルルートのハンドルの取得
      9. Zopeアプリケーションサーバのハンドルの取得
      10. Portalツール
    5. オブジェクトの削除、名前の変更
      1. コンテンツオブジェクトの作成
      2. コンテンツオブジェクトの名前の変更
    6. セキュリティモデル
      1. セキュリティモデル
      2. メソッドやモジュールへのセキュリティの定義
    7. パーミッション
      1. 権限のチェック
      2. オブジェクトの権限の設定
      3. セキュリティのテスト
      4. 権限の操作
    8. ロール
      1. 新しいロールの作成
      2. 現在のユーザの、そのコンテキストにおけるロールの確認
      3. ロールの操作
      4. グループに対する、そのコンテキストにおけるロールの追加
    9. ユーザ
      1. ログインしているユーザの取得
      2. ユーザの削除
      3. ユーザデータベースの操作
      4. すべてのメンバーに対するプロパティの設定
    10. ワークフロー
      1. デフォルトのワークフロー
      2. ワークフローの作成
      3. オブジェクトのワークフローの状態の取得
      4. ワークフローの状態の設定
      5. インストールされているワークフローの取得
      6. あるポータルタイプのデフォルトのワークフローの取得
      7. あるオブジェクトのワークフローの取得
    11. 重要なメソッド
      1. スキーマおよびフィールド
      2. アイテムの型
      3. URL
    12. コンテンツタイプの作成
    13. ビューおよびテンプレート
      1. デフォルトのビューテンプレートの変更
      2. 動的ビューの追加
      3. デフォルトの編集テンプレートの上書き
    14. ページテンプレートおよびウィジェットのふしぎ
      1. 前書き
      2. 名前を動的に設定するマクロの作成
      3. フォルダ内の特定の型のコンテンツの順次処理
      4. フォルダ一覧でのウィジェットのレンダリング
      5. Including a nested context in item
    15. クイックインストーラのサンプル
      1. PloneInstallation
      2. Enabling Large Plone Folders
      3. Hiding actions
      4. Removing permissions and preventing anonymous registration
      5. Adding permissions for custom roles
      6. Adding external method
    16. 開発者向けスクリプト
      1. Windowsでのユニットテスト用のテストランナー
    17. ポータルカタログの問い合わせ
      1. Searching content objects by author and type
      2. Test existence of index and metadata colums
    18. オブジェクトのアクション
      1. Adding a tab to content type
      2. Enabling and disabling actions site wide
      3. Enabling and disabling actions for content type
      4. Overriding edit action
    19. プロパティ
      1. Properties
        1. Setting properties
        2. Updating properties
        3. Testing existence of a property
      2. Site and navigation tree properties
    20. ポートレット
      1. Activating a custom portlet
    21. 新しいコンテンツタイプの作成手順
      1. File system product skeleton
      2. Content type Python module
      3. Initializing the product

Ploneプログラミングレシピ

PloneのオブジェクトをPythonで操作するための基本的な方法をまとめました。

導入

前書き

このチュートリアルでは、Plone/ZopeのオブジェクトをPythonで操作する基本的な手法を説明します。あくまでも基本的な部分だけです。

何か新しい項目(あなたの血と汗と涙の結晶 (^o^))をここに追加したいという場合はコメントをください。私が書き足します。あるいは、ここで説明している内容よりももっとわかりやすい書き方があれば、ぜひ教えてください。

アイテムの作成とコピー

サイトにデータを追加する方法

コンテンツオブジェクトの作成

Ploneのコンテンツオブジェクトを作成するには、親フォルダのinvokeFactoryメソッドを使用します。フォルダ風の(folderish)コンテンツタイプはすべてこのメソッドを持っています。invokeFactoryの引数に渡すのは、コンテンツタイプとそのIDです。さらに、任意の数のオプション引数を渡すことができます。これは、アイテムを作成する際にフィールドのデフォルト値として設定されます。

# employeesというフォルダを作成します。
# オプションの初期値としてtitleを設定しています。
folder = self.portal.invokeFactory("Folder", id="employees", title="Employees")

Ploneのウェブインターフェイス上からオブジェクトを作成する場合には、PortalFolder?.createObjectというメソッドが呼び出されます。createObjectはオブジェクトに一時的なIDを与え、最初にそのオブジェクトが保存されたときにオブジェクトのタイトルにもとづいたIDを設定します。

invokeFactoryのセキュリティチェックや型チェックを迂回する方法

invokeFactoryメソッドは、そのオブジェクトの作成が許可されているかどうかをまずチェックします。ここでチェックするルールは、エンドユーザを念頭に置いたものです。時には、このセキュリティチェックをうっとおしく感じることもあるでしょう。- for example, workaround the global_allow flag in portal types. このような場合に使用するメソッドがCMFPlone.utilsにあります。

from Products.CMFPlone.utils import _createObjectByType

# invokeFactory barks about global_allow flag 
# _createObjectByTypeで、invokeFactoryのチェックを回避します
#self.invokeFactory(CheckoutTool.meta_type, CheckoutTool.id)
_createObjectByType(CheckoutTool.meta_type, self, CheckoutTool.id)

Zopeオブジェクトの作成

Ploneは、Zopeで動く数多くのプロダクトのうちのひとつに過ぎません。Plone以外にもさまざまなZopeプロダクトが存在します。Zopeオブジェクトの作り方は、Ploneのオブジェクトとは多少異なります。

通常、Zope 2.xのオブジェクトにはmanage_addという特別なメソッドがあります。オブジェクトを作成する際にはこのメソッドをコールします。以下に例を示します。

    # ZMySQLDAを使用して、MySQLとの接続を作成します
    # ここでは、self = portal rootです
    from Products.ZMySQLDA.DA import manage_addZMySQLConnection
    if not "mysql_connection" in self.objectIds():
        manage_addZMySQLConnection(self, "mysql_connection", "MySQL connection", "toholampi@localhost root", check=True)

コンテンツオブジェクトのコピー

オブジェクトのコピーは、思っているほど簡単ではありません。実際には以下のような点に注意する必要があります。

  • 一意なオブジェクトID
  • コンテンツオブジェクトの参照
  • 検索インデックスのデータ

ごく一般的なオブジェクトを別のフォルダにコピーする手順は、次のようになります。

            # 元のオブジェクトのコピーを作成します
            classgroup_folder.manage_pasteObjects(proto_folder.manage_copyObjects([sourceId]))
            
            # コピーできたら、そのidを修正します
            classgroup_folder.manage_renameObjects([sourceId], [targetId])
            
            # titleを修正します
            object = classgroup_folder[targetId]
            object.setTitle(targetTitle)
            
            # ナビゲーションツリーや検索インデックスのデータを更新します
            object.reindexObject()

ウェブ上でのコンテンツの作成

Ploneのウェブインターフェイスで新しいオブジェクトを作成する際には、そのオブジェクトのcreateObjectメソッドがコールされます。createObjectは自動的にデフォルトのオブジェクトIDを作成し、"作成中"状態 (object._at_creation_flag = True) にします。

Pythonスクリプト内からcreateObjectを使用したい場合は、次のようにします。

## Script (Python) "add_expertise_area"
##title=Add expertise area button handler
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind state=state
##bind subpath=traverse_subpath
##parameters=id=''
##

# このスクリプトは、自前の「保存」ボタンが押されたときに実行されます。
# 
# これは現在のオブジェクトを保存し、子オブジェクトを作成して
# 編集ビューで開きます。
# 
# 作者: Mikko Ohtamaa <mikko@redinnovation.com>


res = context.content_edit_impl(state, id)
context.plone_log("Got res:" + str(res))

# Add new expertise area and move to its edit form
if res.status == "success":
        
        # Override default "changes saved" message
        state.kwargs["portal_status_message"] = u"Please fill in expertise area details"        
        context.plone_log("Got state:" + str(state))
        
        # Created returns URL for the new object
        # The state object is changed by createObject, so we can discard this information
        created = context.createObject(type_name = "expertiseArea")

フィールドの値の読み書き

コンテンツの状態にアクセスしたり変更したりする方法

フィールドへのアクセス

通常は、独自のコンテンツタイプを作成する場合にはArchetypesのスキーマを使用します。スキーマとは、フィールドとそのプロパティのリストのことです。

以下の例では、コンテンツのスキーマに"mySomeField"というフィールドを作成します。すると、Archetypesが以下のメソッド群を自動生成します(Archetypes/ClassGen.pyを参照ください)。

  • accessor (get メソッド) getMySomeField()
  • mutator (set メソッド) setMySomeField(value)
  • raw accessor (edit accessor とも呼ばれる、文字エンコーディングに関する操作を一切しない get メソッド) getMySomeFieldRaw()

自動生成されたこれれのメソッドは、フィールドのプロパティで上書きすることができます。たとえば、titleにアクセスするにはgetTitle()でなくTitle()も使用できます。

value = myItem.getMagic()

myItem.setMagic(newValue)

Field.getおよびField.setの使用法

accessorやmutatorに直接アクセスできない場合もあるでしょう。あるいは返り値をラップして文字コード変換などを行いたい場合もあるかもしれません。そんな場合にはFieldのgetメソッドおよびsetメソッドを使用します。

f = myItem.getField("myFunnyField")

# Fieldのインスタンスは、同じコンテンツオブジェクト間で共有されます。
# Fieldのメソッドを使用する際には、対象となるオブジェクトをパラメータで指定しなければなりません。

oldValue = f.get(myItem)

f.set(myItem, newValue)

詳細はArchetypes/Field.pyを参照してください。

インデックスの再構築

オブジェクトをすばやく見つけられるように、Ploneの内部では検索カタログとインデックスを管理しています。このインデックスには、オブジェクトの属性の内容が検索しやすい形式で格納されています。場合によっては、オブジェクトに対して行った操作の結果がインデックスに自動反映されないことがあります。このような場合、ナビゲーションツリーや検索機能などがうまく働かなくなってしまいます。

具体的に言うと、object.setTitle()は、PathIndexが作成するナビゲーションツリー内でのタイトルを変更しません。

このような状況を救済するには、変更を行った後にobject.reindexObject()をコールします。

schema = ATContentTypeSchema.copy() + Schema(
    # フィールドリスト
    (     
        TextField('someField',
            widget=StringWidget(
                label='Here is a text value',
                description="Try to set this",
                ),
           ),
        IntegerField('someIntegerField')
    )

class MyType(ATCTContent):
    """
    サンプルとして、いくつかコンテンツを宣言します
    """

    schema = schema
 
    typeDescription= 'MyType custom content type'
    meta_type      = 'MyType'
    archetype_name = 'MyType'

# ...
# そして、そのフィールドを操作します。ここでは
# portal.myfolder.itemを作成したものとします

myItem = portal.myfolder.myitem

myItem.setSomeField("Moo was here")

# カウンタを加算します
intFieldValue = myItem.getSomeIntegerField()
myItem.setSomeIntegerField(intFieldValue + 1)

オブジェクトの型の取得

# オブジェクトの型を、クラスの宣言から取得します
type = object.portal_type

フォルダの中身の一覧、ポータルルートやサイトツールの使用

コンテンツツリーをたどり、サイト内の別の場所にあるオブジェクトを使用する方法

ポータルアイテムの格納方式

すべてのアイテムは、ポータル配下に階層的に格納されています。

オブジェクトのIDを通常のPythonの属性として扱えば、ポータル内のオブジェクトにアクセスすることができます。属性はZopeオブジェクトと透過的に対応し、オブジェクトのIDがそのオブジェクトのURLとなります。Pythonのインターフェイスでは、子オブジェクトとフィールドは区別しません。

たとえば、plone.orgで次のようなスクリプトを実行すれば、このHow-toを参照することができます。

# どうにかしてplone.orgのルートオブジェクトを取得し、それを
# ローカル変数 "portal" に保存する必要があります
documentation = portal.documentation
howTos = getattr(portal, "how-to") # how-toのダッシュが構文上無効なので、getattrを使用する必要があります
myHowTo = getattr(howTos, "manipulating-plone-objects-programmatically")

フォルダの内容の一覧取得

CMFCore/PortalFolder.pyにcontentItemsというメソッドが定義されています。詳細はソースコードをごらんください。

items = folder.contentItems() # 子オブジェクトの情報(id、object)を格納したタプルのリストを返します

このようにしてオブジェクトの一覧を取得するのは、非常に負荷のかかる処理となります。よっぽどのことがない限り、こんなやり方は避けるようにしましょう。詳細な情報は、Plone コアリファレンスの "waking up objects"の部分をごらんください。

特定の型のアイテム一覧の取得

listFolderContentsメソッドは、フォルダ内のオブジェクトを取得します (時間がかかります)。引数contentFilterにはディクショナリを渡します。ここで"portal_type"を指定すると、その型のオブジェクトのみを取得します。

        # このフォルダ内にある、portal_typeが"CourseModulePage"であるすべての型を取得します
        return self.listFolderContents(contentFilter={"portal_type" : "CourseModulePage"})

フォルダのアイテムのIDの取得

IDのみがほしいのならobjectIds()メソッドを使用します。これは、パフォーマンス面でも効率的です。

# フォルダ内のオブジェクトのIDのリストを返します
ids = folder.objectIds()

特定のオブジェクトIDが存在するかどうかの確認

フォルダの中に特定のアイテムが存在するかどうかを調べるには、以下のようなコードを実行します。もし「BTreeFolderであるかどうか」を調べる必要がないのなら、もう少しこれを簡略化することもできます。

# 可能なら、BTreeFolderのAPIを使用します
if base_hasattr(context, 'has_key'): 
    # BTreeFolderのhas_keyは、数値を返します
    return context.has_key('index_html') and True or False 
elif 'index_html' in context.objectIds(): 
    return True 
else: 
    return False 

獲得 (Acquisition)

Ploneでは、多くの場面で「獲得 (Acquisition)」という仕組みを使用しています。これは、クラスツリーにおける継承の概念と似ていますが、獲得の場合はオブジェクトのコンテキスト階層を用いて属性を受け取ります。子オブジェクトから、親コンテナのプロパティを上書きすることができます。獲得を使用する場面としてもっともよくあるのが、権限やプロパティの設定です。通常は、「獲得」用のメソッドを直接コールする必要はありません。

たとえば、

  • left_slotsやright_slotsといったプロパティで表示させたポートレットに対しては「獲得」を用いてアクセスすることができます。left_slotやright_slotは、ポータルのルートで最初に宣言されます。子フォルダ側でこの設定を上書きすることで、これを上書きすることができます。
  • Ploneの階層ごとに、異なる権限を設定することができます。この仕組みのもととなっているのが「獲得」です。

権限については、ZopeのAccessControlモジュールのドキュメント (ソースコード) を参照ください。

プロパティについては、ZopeのOFSモジュールにあるPropertyManagerクラスのドキュメント (ソースコード) を参照ください。

コンテンツの親コンテナの取得

コンテンツの階層をさかのぼる際にも「獲得」を使用することができます。

from Acquisition import aq_parent

parent = aq_parent(context)

あるいは parent = ac_parent(ac_inner(context)) としたくなることもあるかもしれません。これらの使い分けについて、誰かわかりやすく説明できる人はいますか?

ポータルルートのハンドルの取得

ポータルルートは、Ploneのコード上ではportalオブジェクトとして表されます。

クイックインストールスクリプトの関数install(self)において、selfの中身がportalへのハンドルとなります。

ユニットテストの際にはself.portalを使用します。

portalに直接アクセスできない場合は、portal_urlツールと「獲得」を用いて取得することができます。

from Products.CMFCore.utils import getToolByName

# "context"というオブジェクトがあることをご存知でしょう
portal_url = getToolByName(context, "portal_url")
portal = portal_url.getPortalObject()

Zopeアプリケーションサーバのハンドルの取得

Ploneサイトの内部を操作するだけでなく、Zopeアプリケーションサーバに対して直接アクセスしたくなることもあるでしょう。Zopeのルートを取得するには、次のようにします。

app = context.restrictedTraverse('/')

Portalツール

ポータルルート配下にあるツールやユーティリティ群に直接アクセスしなければならないこともあるでしょう。たとえばportal_types (型情報の取得のため) やportal_membership (ログイン情報の取得のため) などが例にあげられます。getToolByNameという関数を使用すると、これらのユーティリティのインスタンスを取得することができます。

from Products.CMFCore.utils import getToolByName

# コンテンツのメソッドでは、このように使用します
def getMySecretVariabl(self)
    plone_utils = getToolByName(self, "plone_utils")

# あるいは、スキンのスクリプト内では、このように使用します
plone_utils = getToolByName(context, "plone_utils")

オブジェクトの削除、名前の変更

Ploneのオブジェクトの削除や名前の変更を、プログラム上で行う方法

コンテンツオブジェクトの作成

コンテンツを削除するには、parent_container.manage_delObjectsを使用します。

Zopeでは、delキーワードを使用するとアイテムを削除することができます。しかし、これを使用してはいけません。delを使用すると、内部のインデックスに不要な情報が残ったままになり、整合性がとれなくなってしまいます。Ploneのオブジェクトを削除する際にhは、常にmanage_delObjectsメソッドを使用するようにしましょう。

# delObjectsは、IDのシーケンスをパラメータとして受け取ります
self.portal.myfolder.manage_delObjects(["myitem"]) # 削除したいアイテムのIDのリストを、パラメータとして渡します

コンテンツオブジェクトの名前の変更

ID(URLとして見える部分)を変更するには、次のようにします。

folder.manage_renameObject(id='my_item_id', new_id='my_item_new_id')

オブジェクトのタイトルを変更するには、次のようにします。

folder.item.setTitle("My new content title")
folder.item.reindexObject()

セキュリティモデル

Zopeのセキュリティモデルの概要を説明し、Pythonのコードでセキュリティを設定する方法を説明します

セキュリティモデル

Pythonのメソッドは、Zopeのセキュリティマネージャによって保護されています。各メソッドは、それぞれ個別にアクセス権限を設定する必要があります。サンドボックス内のコンテキスト(HTTP URLやページテンプレート、あるいはサイトのスクリプト)からサンドボックス外に出るメソッドは、すべてPythonのセキュリティマネージャによるチェックの対象となります。サンドボックスの外部では、セキュリティマネージャによる自動チェックは行われません。毎回このチェックを行うのは、非常に負荷がかかるからです。

すべてのメソッドは、通常はURL経由でアクセスします(http://myhost/mycontent/getMyValue など)。そしていったんセキュリティチェックをクリアすれば、その後再度チェックが行われることはありません。非公開情報を操作したり取得したりするメソッドには、適切な権限を付与しておくことが重要です。

複数の権限を組み合わせて「ロール」として管理することができます。ロールは下位フォルダに継承されます。サブフォルダ側では、親フォルダから継承したロールに対して別の権限を与えることができます。

ロールは、ユーザやグループに対して適用します。ユーザは、サイト内の場所ごとに異なるロールを保持することができます。

メソッドやモジュールへのセキュリティの定義

Zopeにおいて、権限はPythonの文字列形式で設定します。権限の名前をいったん変数に格納した上で、その変数を用いて権限を設定することをお勧めします。そうすれば、名前のタイプミスによるエラーを発見しやすくなります。

Ploneで共通に用いられる権限を以下にまとめます。これらは、CMFCoreのpermissionsモジュールで宣言されています。以下の一覧は、「変数名=変数の中身」形式で表示しています。

  • View = "View"
    • この権限を持っているユーザは、アイテムを閲覧することができます。つまり、閲覧用のクラスメソッドやgetId()、getText()といったメソッドを使用できるようになります。
  • ModifyPortalContent = 'Modify portal content'
    • この権限を持っているユーザは、アイテムを編集することができます。つまり、編集用のクラスメソッドやsetId()、setText()といったメソッドを使用できるようになります。
  • AddPortalContent = 'Add portal content'
    • この権限を持っているユーザは、フォルダにアイテムを追加することができます。
  • ListFolderContents = 'List folder contents'
    • フォルダの中にどんなアイテムがあるのかを見ることができます。
  • AddXXXContent = "Add XXX content"
    • コンテンツタイプごとに、そのタイプのコンテンツを作成するための権限が定義されています。つまり、新しいアイテムを追加するためにはAddPortalContentとAddXXXContentの両方の権限が必要になるということです。

CMFCoreで定義されている権限を使用する例を以下に示します。

from CMFCore import permissions

class REAgent(Member):
    """ Real-estate agent content type """
    
    security = ClassSecurityInfo()

    archetype_name             = 'REAgent'
    meta_type                  = 'REAgent'
    portal_type                = 'REAgent'

    schema = schema

    security.declareProtected(permissions.View, "showImage")
    def showImage(self, blaa):
        """ このメソッドを使用するにはView権限が必要です。View権限を保持している(あるいは継承している)ユーザだけがこのメソッドをコールできます。"""
        ...

モジュールレベルの関数の宣言にはZopeのModuleSecurityを、そしてクラスメソッドの宣言にはClassSecurityを使用します。

  • ClassSecurity.declarePublic("myMethodName") ウェブブラウザも含め、すべての場所からこの関数を使用できます。
  • ClassSecurity.declarePrivate("myMethodName") セキュリティチェックを通過したもののみが使用できます。
  • ClassSecurity.declareProtected(MY_PERMISSION_STRING_CONSTANT, 'myMethodName') 文字列MY_PERMISSION_STRING_CONSTANTで表される権限を保持しているユーザおよびセキュリティチェックを通過したユーザが使用できます。

クラスを宣言した後には、InitializeClassあるいはRegisterTypeをコールしてセキュリティを初期化することを忘れないようにしましょう!

モジュールのセキュリティについては、このチュートリアルも参照してください。

# Zope imports
from AccessControl import ClassSecurityInfo, Unauthorized
from Globals import InitializeClass

# Plone imports
from Products.Archetypes.public import registerType

# Local imports
from Products.MyProduct.permissions import ADD_ISSUES_PERMISSION

class Issue(ATCTContent):
    """ Usability issue for an application """
                
    security = ClassSecurityInfo()    
    
    security.declareProtected(ADD_ISSUES_PERMISSION, 'initializeArchetype')                        
    def doStuff(self, **kwargs):
        """ Set voting enabled initially

        called by the generated addXXX factory in types tool """
        ATCTContent.initializeArchetype(self, **kwargs)
        setattr(self, "enableRatings", True)
        setattr(self, "enableVoting", True)                             

        if(notAllowed == True):
                raise Unauthorized, "Your user can't do this"
        
    security.declarePublic('isVoteable')
    def isVoteable(self):
        """ Do not allow voting of sealed item """
        workflowTool = getToolByName(self, 'portal_workflow')
        #print "Is voteable:" + str(workflowTool.getStatusOf("issue_workflow", self))
        return (workflowTool.getStatusOf("issue_workflow", self)["review_state"] == "in_progress")
        
registerType(Issue, PROJECTNAME)

class MyCustomClass:

    security = ClassSecurityInfo()  

    
    security.declarePublic("blaa") 
    def blaa(self):
        pass

InitializeClass(MyCustomClass)

パーミッション

Zopeの権限をPythonコードで操作する方法

権限のチェック

現在のユーザがそのコンテキストにおける権限を保持しているかどうかを調べる方法です。以下の例ではselfをコンテキストとして扱います。

# portal_membership ツールを使用して権限を確認します
mtool = context.portal_membership
checkPermission = mtool.checkPermission

# checkPermissions は、その権限が付与されているときに true を返します
if checkPermission('Modify portal content', context):
    return "変更できます"

#
# あるいは...
#

if not getSecurityManager().checkPermission(MANAGE_USABILITY_ITEMS_PERMISSION, self):
     raise Unauthorized, "ユーザ " + str(getSecurityManager().getUser()) + " は、アプリケーションフォルダを作成する権限がありません。必要な権限は:" + MANAGE_USABILITY_ITEMS_PERMISSION

オブジェクトの権限の設定

Normally one should never set context object permissions directly in Plone. 正しいやり方は、まずコンテキストオブジェクトの権限を設定するstateを持つワークフローを作成することです。

Zope の内部での権限管理は "権限が付与されたロール + 獲得 (acquisition) が有効な場合に獲得したロール" 形式になっています。AccessControl/Role.py のメソッド manage_permission で、権限を設定します。

    def manage_permission(self, permission_to_manage,
                          roles=[], acquire=0, REQUEST=None):
        """Change the settings for the given permission.

        If optional arg acquire is true, then the roles for the permission
        are acquired, in addition to the ones specified, otherwise the
        permissions are restricted to only the designated roles.
        """

たとえば次のように使用します。

# Plone の一般的な権限については、このモジュールで
# 擬似変数が定義されています
from Products.CMFCore import permissions

def fix_assignment_permissions(context):
    """ student と tutor にのみ許可します
    
    """
    
    # View は Plone 本体の権限のひとつです。これは、
    # そのオブジェクトを誰が閲覧できるのかを表します。
    # このオブジェクトは、studentとtutor、そしてmanagerのみが閲覧できるようにします
    context.manage_permission(
          permissions.View, 
          roles = ["Student", "Tutor", "Manager"],
          acquire=False)

セキュリティのテスト

ユニットテストの際には、PloneTestCaseのメソッドsetRolesを使用します。

これは、アクティブなセキュリティロールをユニットテストのドライバに設定します。ユニットテストでは常にsetRolesを使うようにします。そうすると、セキュリティエラーも捕捉してくれるようになります。

    def testCreateServiceRequest(self):   
        """ Create service request and translate it through all states """
                
        self.setRoles(("Member",))
        
        self.portal.service_requests.invokeFactory("BuyerServiceRequest", id="testRequest")    
        req = self.portal.service_requests.testRequest
        
        self.setRoles((REAGENT_ROLE,))
                        
        workflowTool = self.portal.portal_workflow        
        workflowTool.doActionFor(req, "pick_request")
        workflowTool.doActionFor(req, "close_request")
        
        self.setRoles(("Manager",))
        workflowTool.doActionFor(req, "reopen_request")
        self.setRoles((REAGENT_ROLE,))
        workflowTool.doActionFor(req, "pick_request")
        workflowTool.doActionFor(req, "close_request")

権限の操作

The easiest way to set custom permission for roles is to do it via workflows. Please refer to this tutorial. Note that workflows cannot user permissions before permissions are declared at portal root level.

See Zope/AccessControl/Role.py for methods if you need to do it directly.

This might come in handy:

# Python imports
import types
from StringIO import StringIO

# Zope imports
from AccessControl.Permission import Permission

def addPermissionsForRole(context, role, wanted_permissions):
    """ Add permissions for a role in the context
    
    Parameters:
        @param context Portal object (portal itself, Archetypes item, any inherited from RoleManager)
        @param role role name, as a string
        @param wanted_permissions tuple of permissions (string names) to add for the role    
        
    All wanted_permissions lose their acquiring ability
    """        
    
    assert type(wanted_permissions) == types.TupleType
            
    #print "Doing role:" + role + " perms:" + str(wanted_permissions)
    for p in context.ac_inherited_permissions(all=True):
        name, value = p[:2]
        p=Permission(name,value, context)        
        roles=list(p.getRoles())
            
        #print "Permission:" + name + " roles " + str(roles)
        if name in wanted_permissions:
            if role not in roles:
                roles.append(role)
            p.setRoles(tuple(roles))  
            
def removePermissionsFromRole(context, role, wanted_permissions):
    """ Remove permissions for a role in the context
    
    Parameters:
        @param context Portal object (portal itself, Archetypes item, any inherited from RoleManager)
        @param role role name, as a string
        @param wanted_permissions tuple of permissions (string names) to add for the role    
        
    All wanted_permissions lose their acquiring ability
    """    
    
    assert type(wanted_permissions) == types.TupleType
            
    #print "Doing role:" + role + " perms:" + str(wanted_permissions)
    for p in context.ac_inherited_permissions(all=True):
        name, value = p[:2]
        p=Permission(name,value, context)        
        roles=list(p.getRoles())
            
        #print "Permission:" + name + " roles " + str(roles)
        if name in wanted_permissions:            
            if role in roles:
                roles.remove(role)
            p.setRoles(tuple(roles))   

ロール

ロールの作成、コンテンツオブジェクトやロールの操作

新しいロールの作成

The example below creates a role programmatically

From PloneInstallation RoleInstaller.py

    def doInstall(self, context):
        """Creates the new role
        @param context: an InstallationContext object
        """
        context.portal._addRole(self.role)
        context.logInfo("Added role '%s'" % self.role)
        if self.model:
            # Copies permissions from an existing role
            permissions = self._currentPermissions(context, self.model)
            context.portal.manage_role(self.role, permissions=permissions)
            context.logInfo("Give permissions of '%s' to '%s'" %
                            (self.model, self.role))
        if self.allowed:
            context.portal.manage_role(self.role, permissions=self.allowed)
            context.logInfo("Allowed permissions %s to '%s'" %
                            (', '.join(["'" + p + "'" for p in self.allowed]),
                             self.role))
        if self.denied:
            permissions = self._currentPermissions(context, self.role)
            for p in self.denied:
                permissions.remove(p)
            context.portal.manage_role(self.role, permissions=permissions)
            context.logInfo("Denied permissions %s to '%s'" %
                            (', '.join(["'" + p + "'" for p in self.denied]),
                             self.role))
        return

現在のユーザの、そのコンテキストにおけるロールの確認

user.getId() in context.users_with_local_role('Owner')

ロールの操作

Adding local rules for a user. The role is effective only in the context and nested child objects.

object.manage_addLocalRoles(username, ("My Custom Role",))

グループに対する、そのコンテキストにおけるロールの追加

ユーザ

ログインしているユーザの取得、ユーザデータベースの操作

ログインしているユーザの取得

現在ログインしているユーザとその名前を取得します。

from Products.CMFCore.utils import getToolByName

mt = getToolByName(self, 'portal_membership')
if mt.isAnonymousUser(): # そのユーザはログインしていません
    pass
else:
    member = mt.getAuthenticatedMember()
    username = member.getUserName()

ユーザの削除

Plone 2.5での方法

        try:
            self.portal.acl_users.source_users.doDeleteUser("hr")            
        except KeyError:
            # そのユーザは存在しません
            pass            

メンバーのフルネームの取得

        mt = getToolByName(self, 'portal_membership')
        member = mt.getAuthenticatedMember()
        fullname" : member.getProperty('fullname')

メンバーのメールアドレスの取得

        mt = getToolByName(self, 'portal_membership')
        member = mt.getAuthenticatedMember()
        fullname" : member.getProperty('fullname')

ユーザデータベースの操作

Manipulating users depends a bit what kind of user backend you have

  • Zope internal user database
  • CMFMember or other product which presents users as site content
  • External user database through PlonePAS (e.g. LDAP Windows user accounts)

Ploneを直接使用する例

    def createMember(self, id, pw, email, roles=('Member',)):
        pr = self.portal.portal_registration
        member = pr.addMember(id, pw, roles, properties={ 'username': id, 'email' : email })
        return member

CMFMemberを使用する例

    def createREAgent(self, id):
        md = self.portal.portal_memberdata
        tmp_id = id + '_tmp_id'
        md.invokeFactory(type_name='REAgent', id=tmp_id)
        return md._getOb(tmp_id)                    

すべてのメンバーに対するプロパティの設定

This example shows how to change the editor for all users.

Below code is used from an external method, it was placed as 'switchToKupu.py' inside a product's 'Extensions/' directory. This was used to move users from Epoz to Kupu:

def switchToKupu(self):
    out = []
    # Collect members
    pm = self.portal_membership
    for memberId in pm.listMemberIds():
        member = pm.getMemberById(memberId)
        editor = member.getProperty('wysiwyg_editor', None)
        if editor == 'Kupu':
            out.append('%s: Kupu already selected, leaving alone' % memberId)
        else:
            member.setMemberProperties({'wysiwyg_editor': 'Kupu'})
            out.append('%s: Kupu has been set' % memberId)
    return "\n".join(out)

ワークフロー

ワークフローをプログラム上で扱う方法

デフォルトのワークフロー

For Plone stock workflow state ids and transition ids see DCWorkflow/Default.py

ワークフローの作成

To create or manipulate workflows please refer to this tutorial.

オブジェクトのワークフローの状態の取得

If you want to read the workflow state of an object, use the following snippet:

        workflowTool = getToolByName(self.portal, "portal_workflow")                
        # Returns workflow state object
        status = workflowTool.getStatusOf("plone_workflow", object)
        # Plone workflows use variable called "review_state" to store state id
        # of the object state
        state = status["review_state"]
        assert state == "published", "Got state:" + str(state)

ワークフローの状態の設定

To set workflow state programmatically, you need to use WorkflowTool

        portal.invokeFactory("SampleContent", id="sampleProperty")                    
        
        workflowTool = getToolByName(context, "portal_workflow")        
        workflowTool.doActionFor(portal.sampleProperty, "submit")

WorkflowTool also can list available actions. Note that there can be several workflows per object. This is important to know when retrieving the current workflow state.

インストールされているワークフローの取得

Gets the list of ids of all installed workflows. Test if there is one particular present.

  # Get all site workflows
  ids = workflowTool.getWorkflowIds()
  self.failUnless("link_workflow" in ids, "Had workflows " + str(ids))

あるポータルタイプのデフォルトのワークフローの取得

 # Get default workflow for the type
 chain = workflowTool.getChainForPortalType(ExpensiveLink.portal_type)
 self.failUnless(chain == ("link_workflow",), "Had workflow chain" + str(chain))        

あるオブジェクトのワークフローの取得

How to test which workflow the object has

        # See that we have a right workflow in place
        workflowTool = getToolByName(context, "portal_workflow")      
        # Returns tuple of all workflows assigned for a context object
        chain = workflowTool.getChainFor(context)
        
        # there must be only one workflow for our object
        self.failUnless(len(chain) == 1)
        
        # this must must be the workflow name
        self.failUnless(chain[0] == 'link_workflow', "Had workflow " + str(chain[0]))

重要なメソッド

Getting content type, URL, workflow state, etc.

スキーマおよびフィールド

Getting Field from content

field = content.getField("myFieldName")

Getting schema

schema = content.getSchema()

Iterating through fields

for id in schema.keys():
        field = schema[id]

アイテムの型

Getting item type

item.getTypeInfo().getId() # return factory type information id, e.g. portal_type attribute

URL

If you want to give URLs for your content object in page templates and page scripts

url = item.absolute_url()

or in page template

<a href="#" tal:attributes="href string:${here/absolute_url}">

コンテンツタイプの作成

Plone用の独自のコンテンツタイプを作成する方法

Martin Aspeliが、すばらしいRichDocumentチュートリアルを書いています。私が何を書いてもその二番煎じになってしまいそうなので、ここでは省略します。

ビューおよびテンプレート

Plone has different sets of views appearing for each content type. The basic views are "view" and "edit". This chapter tells how to add and manipulate views.

デフォルトのビューテンプレートの変更

The most common use case is that one wishes to have a customized view template for a new content type.

First, change "default_view" attribute in AT class definition. This attribute tells what page template is used to view the content object.

class consultantInformation(ATCTFolder):
    """
    """
    default_view = "consultant_view"

    schema = schema

動的ビューの追加

Dynamic views appear in the object's Display menu. The manager can override default view mode for the object. For example, Plone ships with "Album view" for folders which makes folders behave like photo albums, showing thumbnailed images.

  1. Add your custom template to supplied views of your content class
      class WorkPackageFolder(ATFolder):
          """ Contains work package items
          """
          schema = schema
          
          filter_content_types = True
          
          typeDescription= 'Work package folder'
          meta_type      = 'WorkPackageFolder'
          archetype_name = 'Work package folder'
              
          # Generate user friendly id from item title during creation
          # Effective only for ATContentTypes based classes
          _at_rename_from_title = True
          
          suppl_views    = ('my_template_name',)
  1. Create a custom view template my_template_name.pt. Good starting points are base_view.pt in Archetypes product and various templates in ATContentTypes product.
  2. Create my_template_name.pt.metadata file which will contain user readable label for your view and other information. This must be in the same folder with the view template.
      [default]
      title = My view name

デフォルトの編集テンプレートの上書き

The default edit template is called "base_edit.cpt". Here are instructions how you replace it for your AT class to add custom text on the template.

  • Copy base_edit.cpt and base_edit.metadata to your skins directory
  • Rename them to specific to your item, e.g. wnc_edit.cpt and wnc_edit.metadata
  • Edit wnc_edit.cpt. The following exampe adds a new button Add expertise area next to Save. Note that <input> name must be in form of "form.button.xxx".
        <metal:use_body use-macro="body_macro">

            <metal:block fill-slot="buttons"
                   tal:define="fieldset_index python:fieldsets.index(fieldset);
                               n_fieldsets python:len(fieldsets)">

                <input tal:condition="python:fieldset_index &gt; 0"
                       class="context"
                       tabindex=""
                       type="submit"
                       name="form_previous"
                       value="Previous"
                       i18n:attributes="value label_previous;"
                       tal:attributes="tabindex tabindex/next;
                                       disabled python:test(isLocked, 'disabled', None);"
                       />
                <input tal:condition="python:fieldset_index &lt; n_fieldsets - 1"
                       class="context"
                       tabindex=""
                       type="submit"
                       name="form_next"
                       value="Next"
                       i18n:attributes="value label_next;"
                       tal:attributes="tabindex tabindex/next;
                                       disabled python:test(isLocked, 'disabled', None);"
                       />
                <input class="context"
                       tabindex=""
                       type="submit"
                       name="form_submit"
                       value="Save"
                       i18n:attributes="value label_save;"
                       tal:attributes="tabindex tabindex/next;
                                       disabled python:test(isLocked, 'disabled', None);"
                       />

                <input class="context"
                       tabindex=""
                       type="submit"
                       name="add_expertise_area"
                       value="Add expertise area"
                       />                       
                       
                <input class="standalone"
                       tabindex=""
                       type="submit"
                       name="form.button.cancel"
                       value="Cancel"
                       i18n:attributes="value label_cancel;"
                       tal:attributes="tabindex tabindex/next"
                       />
            </metal:block>        
        
        
        </metal:use_body>
  • Add following to your content type class
from Products.ATContentTypes.content.base import updateActions, updateAliases

class WNCCompany(ATCTContent):
    """ Consultancy company listing entry
    """
    security = ClassSecurityInfo()
    
    # This name appears in the 'add' box
    archetype_name = 'Consultancy company'

    meta_type = portal_type = 'WNCCompany'    
    
    global_allow = True
        
    _at_rename_after_creation = True

    schema = schema
    
    # Override default edit view
    actions = updateActions(ATCTFolder, (
        { 'id': 'edit',
           'name': 'Edit',
           'action': 'string:${object_url}/wnc_edit',
           'permissions': (permissions.ModifyPortalContent,),
        },
        ))    

ページテンプレートおよびウィジェットのふしぎ

How to perform often requested tricks with Archetypes widgets and page templates

前書き

テンプレートの内容をKupuにコピペしたときに、フォーマットがくずれてしまっているかも……。

名前を動的に設定するマクロの作成

Try this code (also, example in the section below):

         <tal:block tal:define="macro_path python: path('here/%s/macros' % page_template_name);
                                callable_macro macro_path/my_custom_macro_name;">
                                           
                        
                                <tal:use-macro metal:use-macro="callable_macro" />
                            
          </tal:block>

フォルダ内の特定の型のコンテンツの順次処理

The following macro serves as a base how to iterate certain content types in a folder

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
    lang="en">
    
    <!-- per_content_type_renderer macro definition
    
    List certain content types and calls a target macro for them.
    Target macro is given as a page template filename which contains the macro.
    
    Takes arguments:
    
    wanted_item_type: string, content type id. 
                                  Be careful with space padding in template code.
    
    view_macro: which macro to be called, template file basename, 
                        file contains 'listing_core' macro

    Author: Mikko Ohtamaa

    http://www.redinnovation.com
    
    -->
    
    <body>

        <metal:macro define-macro="per_content_type_renderer">
            <tal:foldercontents 
                define="contentFilter    contentFilter|request/contentFilter|nothing;
                limit_display    limit_display|request/limit_display|nothing;
                more_url         more_url|request/more_url|string:folder_contents;
                contentsMethod   python:test(here.portal_type=='Topic', here.queryCatalog, here.getFolderContents);
                folderContents   folderContents|python:contentsMethod(contentFilter, batch=True);
                use_view_action  site_properties/typesUseViewActionInListings|python:();
                over_limit       python: limit_display and len(folderContents) > limit_display;
                folderContents   python: (over_limit and folderContents[:limit_display]) or folderContents;
                batch            folderContents">
                <tal:listing condition="folderContents">
     
                    <div tal:repeat="item folderContents">
                        <tal:block tal:define="item_url item/getURL|item/absolute_url;
                            item_type           item/portal_type;
                            item_object         item/getObject;
                            item_creator        item/Creator;
                            macro_path python: path('here/%s/macros' % view_macro);
                                        callable_macro macro_path/listing_core;">
                                           
                            <tal:activity tal:condition="python: item_type == wanted_item_type">                           
                                <tal:use-macro metal:use-macro="callable_macro" />
                            </tal:activity>
                            
                        </tal:block>
                    </div>
                </tal:listing>
            </tal:foldercontents>
        </metal:macro>            
       
        </body>
</html>

フォルダ一覧でのウィジェットのレンダリング

This is an often heard request - one wants to render a widget outside Archetypes rendering flow

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
    lang="en">
    
    <body>
        <tal:define metal:define-macro="render_field">
            
            <!-- Calling Archetypes widget renderer directly for a certain field
            
              This snippet is useful if one wants to render AT widgets outside their context 
              object, e.g. in a folder summary view.
            
              The following code takes an arbitary AT content object in item_object variable.
              
              It checks whether this item object has a field "Staff" and then calls 
              Staff default widget renderer. We need to perform the trick of redeclaring
              context variable which might confuse some code (e.g. permissions) so 
              be careful.
              
              Takes arguments:
              
              target_item: Object whose field we are rendering
              
              field_name: Field name as a string
              
              use_label: True or False whether label should be rendered
              
              Author: Mikko Ohtamaa
              
              www.redinnovation.com            
            
              -->

                        <tal:has-field tal:condition="python: field_name in target_item.schema">
                                <tal:field-context tal:define="context python: target_item;
                                                                field python: target_item.schema[field_name];
                                                                widget_view python: target_item.widget(field.getName(), mode='view', use_label=use_label);
                                            field_macros here/widgets/field/macros;
                                        label_macro view_macros/label | label_macro | field_macros/label;
                                            data_macro view_macros/data | data_macro | field_macros/data;
                                                                ">
                
                                    <div tal:define="fieldtypename python:field.getType().split('.')[-1]"
                                                tal:attributes="class string:field ArchetypesField-${fieldtypename};
                            id string:archetypes-fieldname-${field/getName}">
                            
                                <tal:if_perm condition="python:'view' in widget.modes and 'r' in field.mode and field.checkPermission('r',here)">
                                                <tal:if_use_label tal:condition="python: use_label">
                                                <metal:use_label use-macro="label_macro" />
                                                </tal:if_use_label>
                                                
                                            <metal:use_data use-macro="data_macro|default" />
                                        </tal:if_perm>
                                    </div>
                            </tal:field-context>
            </tal:has-field>

        </tal:define>     
        </body>
</html>

Example how to use the page template above:

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
    lang="en">
    
    <body>
        <tal:define metal:define-macro="my_funny_macro">
            <h1>Render item_object.Staff field</h1>
            
            <tal:field-render-core
                tal:define="target_item python: item_object;
                                        field_name string:Staff;
                                        use_label python: False">
                            <tal:call-renderer metal:use-macro="here/render_field/macros/render_field" />
                </tal:field-render-core>

        </tal:define>
        </body>
</html>

Including a nested context in item

The following snippet will include code from an nested item in the context folder. The nested item will be rendered in a custom widget view macro.

    <!-- VIEW -->
    <metal:define define-macro="view">
        <tal:no-fees condition="python: not 'course-fees' in context.objectIds()">
                <p tal:condition="not: isAnon">
                        Please add Fees page with id "course-fees" to see it here. 
                        You can use contents tab/rename button to change the id of the object.
                        This messages is visible for editors only.
                </p>
        </tal:no-fees>
        
        <tal:has-fees condition="python:'course-fees' in context.objectIds()">
                <tal:new-context define="context python:context['course-fees']; here python:context['course-fees']">
                        <metal:body use-macro="here/base/macros/body" />
                </tal:new-context>
        </tal:has-fees>
    </metal:define>

クイックインストーラのサンプル

Each Plone product has quick installer script which prepares the portal for the product. Here are some useful snippets which you can reuse.

PloneInstallation

I sincerely recommend using PloneInstallation product in quick installer scripts. It has many classes needed not to reinvent the wheel every time one writes a quick installer script.

Enabling Large Plone Folders

Large Plone Folders (also known as BTreeFolders) use binary trees as the folder index. This makes folder look ups faster on large item counts. By default, creation of Large Plone Folders is disabled. To enable it, run this code

    # Allow creation of large folders    
    lpf = portal.portal_types.getTypeInfo("Large Plone Folder")    
    lpf.global_allow = True    

Hiding actions

If you use quick installer script to customize your Plone site you might want to hide certain actions from the end users

    # Hide some actions
    actionsTool = self.portal_actions
    act = actionsTool.getActionInfo("document_actions/print")
    act.condition = "python: False"
    
    act = actionsTool.getActionInfo("document_actions/sendto")
    act.condition = "python: False"

Removing permissions and preventing anonymous registration

This code removes the permission to create new users from anonymous visitors. The wanted side effect is that Join link also disappears.

# Python imports
import types

# Zope imports
from AccessControl.Permission import Permission

# Plone imports
from Products.CMFCore.permissions import *

def removePermissionsFromRole(context, role, wanted_permissions):
    """ Remove permissions for a role in the context.
    
    Parameters:
        @param context Portal object (portal itself, Archetypes item, any inherited from RoleManager)
        @param role role name, as a string
        @param wanted_permissions tuple of permissions (string names) to add for the role    
        
    All wanted_permissions lose their acquiring ability
    """    
    
    assert type(wanted_permissions) == types.TupleType
            
    #print "Doing role:" + role + " perms:" + str(wanted_permissions)
    for p in context.ac_inherited_permissions(all=True):
        name, value = p[:2]
        p=Permission(name,value, context)        
        roles=list(p.getRoles())
            
        #print "Permission:" + name + " roles " + str(roles)
        if name in wanted_permissions:            
            if role in roles:
                roles.remove(role)
            p.setRoles(tuple(roles))             

# Prevent registration at the site
removePermissionsFromRole(self, "Anonymous", (AddPortalMember,))

Adding permissions for custom roles

The following snippets allows you to add permissions for roles

# Python imports
import types

# Zope imports
from AccessControl.Permission import Permission

# Plone imports
from Products.CMFCore.permissions import *

def addPermissionsForRole(context, role, wanted_permissions):
    """ Add permissions for a role in the context.
    
    Parameters:
        @param context Portal object (portal itself, Archetypes item, any inherited from RoleManager)
        @param role role name, as a string
        @param wanted_permissions tuple of permissions (string names) to add for the role    
        
    All wanted_permissions lose their acquiring ability
    """        
    
    assert type(wanted_permissions) == types.TupleType
            
    #print "Doing role:" + role + " perms:" + str(wanted_permissions)
    for p in context.ac_inherited_permissions(all=True):
        name, value = p[:2]
        p=Permission(name,value, context)        
        roles=list(p.getRoles())
            
        #print "Permission:" + name + " roles " + str(roles)
        if name in wanted_permissions:
            if role not in roles:
                roles.append(role)
            p.setRoles(tuple(roles))  

# Make anonymous link submitting possible
addPermissionsForRole(self.link_pool, "Anonymous", (AddPortalContent,))

Adding external method

    # Add external method
    #
    # External methods are Python code which lie in Zope content
    # structure. Each external method has a module in Extensions folder
    # and Zope object in Zope. You can call external methods by typing 
    # in URL directly. External methods bypass Zope security mechanism.
    #
    # Usually external methods are used for automated maintenance tasks.
    #
    # 
    # Following adds a function from poll.py which is in the product's Extensions folder
    #
    # self = portal root
    from config import PROJECTNAME
    from Products.ExternalMethod.ExternalMethod import manage_addExternalMethod
    if not "poll_sql" in self.objectIds():
        manage_addExternalMethod(self, "poll_sql", "Poll external SQL source", PROJECTNAME + ".poller", "poll_sql")

開発者向けスクリプト

Command line scripts which are useful in product development

Windowsでのユニットテスト用のテストランナー

Since Plone 2.5.1 invoking per product unit test modules has become a major pain.

NOTE: This is only necessary on MS Windows. Linux, OSX and other *NIX platforms can run the tests normally from the commandline.

Here is a short .bat file which allows one to run unit tests within the context of one product.

@echo off

REM Invoking unit test directly doesn't work anymore on Plone 2.5.1
REM See http://plone.org/documentation/error/attributeerror-test_user_1_

set PYTHON=d:\python24\python.exe
set ZOPE_HOME=F:\workspace\plone-2.5.1\Zope-2.9.6\Zope
set INSTANCE_HOME=F:/workspace/plone-2.5.1/instance
set SOFTWARE_HOME=%ZOPE_HOME%\lib\python
set CONFIG_FILE=%INSTANCE_HOME%\etc\zope.conf
set PYTHONPATH=%SOFTWARE_HOME%
set TEST_RUN=%ZOPE_HOME%\bin\test.py

"%PYTHON%" "%TEST_RUN%" --config-file="%CONFIG_FILE%" --usecompiled -vp --package-path=%INSTANCE_HOME%/Products/lsmintra Products.lsmintra

ポータルカタログの問い合わせ

Portal catalog provides search indexing information for Plone site. Portal catalog queries are much faster than walking through objects manually,

Searching content objects by author and type

The following snippet will perform a search which returns all items for a certain type and a certain creator.

# Search site for a consultant profile whose creator the current
# user is
#

from Products.CMFCore.utils import getToolByName

portal_catalog = getToolByName(context, 'portal_catalog')
mt = getToolByName(context, 'portal_membership')
if mt.isAnonymousUser(): 
    # the user has not logged in
    return None
else:
    member = mt.getAuthenticatedMember()
    username = member.getUserName()

# Please refer to portal_catalog tool
# Zope management interface for default Plone seach indexes
query = {}
query["Creator"] = username
query["Type"] = "Consultant Profile"

# Return brain objects for search results
brains = portal_catalog.searchResults(**query)

context.plone_log("Got results:" + str(brains))

if len(brains) > 0:
    # Return the real object of the first search hit
    return brains[0].getObject()
else:
    # Np hits - no profile created yet
    return None

Test existence of index and metadata colums

    # Test if catalog has a search index
    if not "getFirmName" in catalog_tool.indexes():
        catalog_tool.manage_addIndex("getFirmName", "ZCTextIndex", extra)
        
    # Test if catalog has a metadata column
    if not "getSummary" in catalog_tool.schema():
        catalog_tool.manage_addColumn("getSummary")

オブジェクトのアクション

Actions are state changing triggers users perform on objects. For example, edit object, copy or print are actions. This chapter describes how to add new actions and manipulate existing actions.

Adding a tab to content type

あとで書く

Enabling and disabling actions site wide

def disable_actions(portal):
    """ Remove unneeded Plone actions 
    
    @param portal Plone instance
    """
    
    # getActionObject takes parameter category/action id
    # For ids and categories please refer to portal_actins in ZMI
    actionInformation = portal.portal_actions.getActionObject("document_actions/rss")
    
    # See ActionInformation.py / ActionInformation for available edits
    actionInformation.edit(visible=False)

Enabling and disabling actions for content type

A sample code to disable few stock Plone actions. The example product RTFExport available here.

from Products.ATContentTypes.content.base import updateActions, updateAliases

class Employee(ATCTContent):
    """ Employee record
    
    
    """
 
    # Add RTF export action icon for the object
    # Hide properties and sharings tabs
    # Hide cut and copy actions
    actions = updateActions(ATCTContent,
        (
        {
        'id'          : 'export_rtf',
        'name'        : 'Export as RTF',
        'action'      : 'string:$object_url/export_rtf',
        'permissions' : (View,),
        'category'    : "document_actions",
        },
         {
        'id'          : 'metadata',
        'visible'     : False,
         },
         {
        'id'          : 'local_roles',
        'visible'     : False,        
         },
        {
        'id'          : 'sendto',
        'visible'     : False,        
         },
        {
        'id'          : 'cut',
        'visible'     : False,        
         },                           
         {
        'id'          : 'copy',
        'visible'     : False,        
         },                 
        )
        )

Overriding edit action

An usual use case is using custom edit form. Add the following snippet to your content class definition to use consultant_edit form as the edit form:

    actions = updateActions(ATCTFolder, (
        { 'id': 'edit',
           'name': 'Edit',
           'action': 'string:${object_url}/consultant_edit',
           'permissions': (permissions.ModifyPortalContent,),
        },
        ))    

プロパティ

Properties are flexible key-value pairs assigned to content types and tools. Properties are passed to child content objects via acquisition.

Properties

Properties are a special kind of acquired values. Properties have automatically generated user interface in Zope Management Interface to deal with them. If you hit any folder or object in ZMI it has properties tab were can fiddle around with these.

The most common properties one would want to change are probably left_slots and right_slots which control the appearing of portlets in Plone 2.5.x. (Plone 3.0 has reworked portlet system).

Setting properties

Setting properties to an object causes it to override parent properties in an acquisition chain. Properties must not exist on the object before calling _setProperty. _setProperty takes property type which can be found out on ZMI Properties tab.

Overriding portlet settings in a subfolder. No portlets are used for items inside this folder:

    data_storage._setProperty('left_slots', [], 'lines')    
    data_storage._setProperty('right_slots', [], 'lines')

Updating properties

Existing properties can be updated with _updateProperty. This only works if properties have been created using set before. Properties must exist on the target object itself, inherit properties are not count in.

Updating properties left_slots and right_slots for the portal root:

        portal._updateProperty("left_slots", ["here/portlet_navigation/macros/portlet","here/portlet_login/macros/portlet"])
        portal._updateProperty("right_slots", [])

Testing existence of a property

Use object.hasProperty.

The following example code will set or update a property.

    # The following code will create or update property.
    # Update default view page template for assigments folder
    # default_page property tells the custom page 
    # template used to render this particular content object
    # in view mode
    if not assignments.hasProperty("default_page"):
        # Create the property        
        assignments._setProperty("default_page", "")

    # Override assigments value (old or new created)
    assignments._updateProperty("default_page", "assigments_view")

Site and navigation tree properties

There is a special tool portal_properties which manages most of Plone's site wide properties. Please refer to its content by peeking it in ZMI.

Example: Modifying a navigation tree behavior

self.portal_properties.navtree_properties._updateProperty("topLevel", 1)

ポートレット

How to deal with portlets

Activating a custom portlet

This is Plone 2.x way. Plone 3.0 has revamped portlet system. The item shows portlets which are defined in left_slots or right_slots properties.

    # Activate shopping portlet 
    # This can be done for any folder
    # self = portal root
    right_slots = self.right_slots
    new_portlet_macro = "here/portlet_shopper/macros/portlet"
    if not new_portlet_macro in right_slots:
        self._updateProperty("right_slots", right_slots + (new_portlet_macro,))

新しいコンテンツタイプの作成手順

The check list what you need to do when you create new content types for Plone.

This applies for Plone 2.1.x and still works in Plone 2.5.x. It is encouraged to use newer mechanisms provided by Five subsystem when you start new products from scracth. Please read Martin Aspeli's excellent tutorial about the subject.

Archetypes is a Plone subsystem to define new content types. Content types are Python classes which have special attributes described by Archetypes product. The most import of them is schema which defines what fields your content type has. Archetypes reference manual is handy.

File system product skeleton

You need a file system product where new content types is added. You can take the existing product and rip its flesh away or use examples provided by Archetypes manual.

Content type Python module

Create a Python module containing your content type class declaration

  • Create a .py module to the "content" folder of the product.
  • Add Necessary dependency imports for fields, widgets, parent schema and parent class

Example

# Plone imports
from Products.Archetypes.public import *
from Products.ATContentTypes.content.base import ATCTContent
from Products.ATContentTypes.content.base import updateActions, updateAliases
from Products.ATContentTypes.content.schemata import ATContentTypeSchema
from Products.ATContentTypes.content.schemata import finalizeATCTSchema

# Local imports
from Products.MyCustom.BulletField import BulletField
from Products.MyCustom.BulletWidget import BulletWidget

from Products.MyCustom.config import *
  • Create the schema definition for your content type. See Archetypes manual for available fields, widgets and their properties.

Example

schema=ATFolderSchema.copy() + Schema((
                                                                
        TextField('isItForMe',
               default='',
               searchable=True,
               widget=RichWidget(
                    label='Is it for me?',
                    )
                ),
    
        TextField('benefits',
                       default='',
                       searchable=True,
                       widget=RichWidget(
                             label='What are the benefits of taking this course?',                                             
                             )
                        ),
        
        LinksField('whatDoILearn',
                       default='',
                       searchable=True,
                       widget=RichWidget(label='What do I learn?', macro="what_do_i_learn_widget.pt")
                        ),
    ))
finalizeATCTSchema(schema)

The class definition defines security and general properties of your content type.

Example:

class CoursePage(ATFolder):
    """ Courses page content type
    
    """
    
    # Internal programming id which Plone uses to refer this type
    portal_type = meta_type = 'CoursePage' 
    
    # User readable name in "Add new content" drop down menu
    archetype_name = 'Course page'  
    
    # Help text for Add new content drop down menu
    typeDescription = "A course description telling what students should expect for this course"
    
    # Fields used in this content type (as defined above)
    schema = schema
        
    # If true, this content type can be created anywhere at the site
    global_allow = True
    
    
    # We limit what kind of items are allowed in this folderish content type
    filter_content_types = True
    
    # List of allowed content types
    allowed_content_types = [ "GeneralLSMPage", "CourseModulePage", "CourseModePage", "GeneralLSMPage2", "CourseFeesModePage" ]

You need to use registerType to register your class definition for Zope security manager.

# CoursePage is your Python class
# PROJECTNAME is the name of your product and it's usually declared in config.py    
registerType(CoursePage, PROJECTNAME)

Initializing the product

To make your content type available for Plone

  • You need to import it during the Plone start-up sequence
  • You need to run the quick installer script to create persistent portal_type entries for your content type

In init.py of your product, import the new content type module so that Zope security manager is initialized for it

"""
            
    Copyright 2006 xxx
    
"""

__author__  = 'Mikko Ohtamaa <mikko@redinnovation.com>'
__docformat__ = 'epytext'

from Products.Archetypes.public import process_types, listTypes
from Products.Archetypes.ArchetypeTool import getType
from Products.CMFCore.DirectoryView import registerDirectory
from Products.CMFCore import utils as CMFCoreUtils

from config import *
from Permissions import *

def initialize(context):
    """ Registers all classes to Zope
        
    @param context Zope/App/ProductContext instance
    
    """
        
    # initialize security context
    from content import MyCustomContentTypeModule

    # Register our skins directory - this makes it available via portal_skins.
    registerDirectory(SKINS_DIR, GLOBALS)
    
    # helper function to go through all content types in your product
    content_types, constructors, ftis = process_types(
        listTypes(PROJECTNAME),
        PROJECTNAME)

    # Initialize content types
    CMFCoreUtils.ContentInit(
        PROJECTNAME + ' Content',
        content_types      = content_types,
        permission         = PermissionNameToCreateThisContent,
        extra_constructors = constructors,
        fti                = ftis,
        ).initialize(context)    

Then, you need to remember to register types in Extension/Install.py quick installer script. This creates persistent portal_type entries for your content type. Each time you change the general properties of your content type (everything except schema) you need to rerun the quick installer.

"""

    Extension/Install.py quick installer script
            
    Your Copyright line here
    
"""

__author__  = 'Mikko Ohtamaa <mikko@redinnovation.com>'
__docformat__ = 'epytext'

# Python imports
from cStringIO import StringIO

# Plone imports
from Products.Archetypes.public import listTypes
from Products.Archetypes.Extensions.utils import installTypes, install_subskin

# Local imports
from Products.MyProduct.Extensions.utils import *
from Products.MyProduct.config import *

def install(self):
    """ This is called by Plone quick installer tool 
    
    @param self portal instance object
    
    @return String which is added for the installer log
    """
    out = StringIO()

    # Register layout files in the portal
    install_subskin(self, out, GLOBALS)           
    
    registerStylesheets(self, out, STYLESHEETS)
    registerScripts(self, out, JAVASCRIPTS)
    
     # Register Archetypes types in the portal
    installTypes(self, out, listTypes(PROJECTNAME), PROJECTNAME)
    
    print >> out, "Installation completed."
    return out.getvalue()


def uninstall(self):
    # TODO
    out = StringIO()

添付ファイル