Wednesday, May 4, 2011

Creating Plone content when installing / upgrading a product

[update 2012-05-23: link to GS import/upgrade at http://collective-docs.readthedocs.org/en/latest/components/genericsetup.html#custom-installer-code-setuphandlers-py]
[update 2013-03-23: underscore added to function call]

Following up on my previous post, i've extended the method to create objects a bit.

Short reminder: this method is intended for use when Generic Setup (profiles/default/structure) can't create content in the way you want, for example when you have custom content types, or want to change "exclude_from_navigation" settings or workflow state. This article assumes you will call your setuphandler.py's methods from a Generic Setup import or upgrade step.

## setuphandlers.py

from Products.CMFPlone.utils import _createObjectByType
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.WorkflowCore import WorkflowException

def setupVarious(context):

    # Ordinarily, GenericSetup handlers check for the existence of XML files.
    # Here, we are not parsing an XML file, but we use this text file as a
    # flag to check that we actually meant for this import step to be run.
    # The file is found in profiles/default.

    if context.readDataFile('My.Product_various.txt') is None:
        return

    # Add additional setup code here

def _my_structure():
    return [
        {   'id': 'new-folder', 
            'title': 'New Folder',
            'description': 'Folder for authenticated users',
            'type': 'Folder',
            'workflow_transition': 'retract',
            'exclude_from_nav': True,
            },
        ]

def _createObjects(parent, children):
    """This will create new objects, or modify existing ones if id's and type
    match.

    This takes two arguments: the parent to create the content in, and the
    children to create.

    Children is a list of dictionaries defined as follows:

    new_objects = [
        {   'id': 'some-id', 
            'title': 'Some Title',
            'description': 'Some Description',
            'type': 'Folder',
            'layout': 'folder_contents',
            'workflow_transition': 'retract',
            'exclude_from_nav': True,
            'children': profile_children,
            },
        ]
    
    * layout:               optional, it sets a different default layout
    * workflow_transition:  optional, it tries to start that state transition
        after the object is created. (You cannot directly set the workflow to 
        any state, but you must push it through legal state transitions.)
    * exclude_from_nav:     optional, excludes item from navigation
    * children:             optional, is a list of dictionaries (like this one)

    """

    parent.plone_log("Creating %s in %s" % (children, parent))

    workflowTool = getToolByName(parent, "portal_workflow")

    existing = parent.objectIds()
    for new_object in children:
        if new_object['id'] in existing:
            parent.plone_log("%s exists, skipping" % new_object['id'])
        else:
            _createObjectByType(new_object['type'], parent, \
                id=new_object['id'], title=new_object['title'], \
                description=new_object['description'])
        parent.plone_log("Now to modify the new_object...")
        obj = parent.get(new_object['id'], None)
        if obj is None:
            parent.plone_log("can't get new_object %s to modify it!" % new_object['id'])
        else:
            if obj.Type() != new_object['type']:
                parent.plone_log("types don't match!")
            else:   
                if new_object.has_key('layout'): 
                    obj.setLayout(new_object['layout'])
                if new_object.has_key('workflow_transition'): 
                    try:
                        workflowTool.doActionFor(obj, 
                            new_object['workflow_transition'])
                    except WorkflowException:
                        parent.plone_log(
                            "WARNING: couldn't do workflow transition")
                if new_object.has_key('exclude_from_nav'):
                    obj.setExcludeFromNav(new_object['exclude_from_nav'])
                obj.reindexObject()
                children = new_object.get('children',[])
                if len(children) > 0:
                    _createObjects(obj, children)

def createContent(context):
    portal = getToolByName(context, 'portal_url').getPortalObject()
    _createObjects(portal, _my_structure())

3 comments:

Chris said...

Thanks for this. I was successful at getting your example to work by using context.getSite() instead of getToolByName(context,'portal_url').

Anonymous said...

Thanks man!
I found a bug in the recursive call to _createObjects, the '_' character is missing at the beginning of the function name!

;)

Anonymous said...

Thanks for this.
Requests to add functionality:
Restricting available types per folder(container) instance