This is a rather technical post, full of code. Faint of hearth you are advised!
The story
Not all the content types are designed from the beginning with a container behaviour, a feature that can become a requirement in consequence of specification changes.
It happened also to me and in my case I had to deal with dexterity based content types. I started googling around and found this unresolved question on the dexterity mailing list.
Starting from the suggestions in the thread, I managed to find out a nifty solution that reveals some interesting aspects of the software we work with.
Creating the dexterity type
First of all let's create through the web the Example content dexterity type:
Then let's create the "Example 1" object:
Screwing it all up
Going to the portal_type edit form we can make "Example content" become a folderish object by modifying it's content type class, just replace "Item" with "Container".
To make something interesting it is probably advisable to play with the "Filter content types?" and "Allowed content types" properties, allowing the creation of objects inside this container.
Once this is done the newly created objects show a folderish behaviour and view:
But the old one is completely unchanged and has to be migrated!
The objects have clearly a different nature, because they are instances of different classes. Going into a pdb we can clearly see it:
(pdb) portal['example-1'].portal_type
'example_content'
(pdb) portal['example-2'].portal_type
'example_content'
(pdb) portal['example-1']
<Item at /Plone/example-1>
(pdb) portal['example-2']
<Container at /Plone/example-2>
The portal type is the same but:
- "Example 1" is an instance of plone.dexterity.content.Item
- "Example 2" is an instance of plone.dexterity.content.Container.
Building the box
It is time to fold the paper sheet in to a box!
Aspeli's suggestion of replacing the __class__ attribute is a good starting point, but I found out that restarting the instance swallowed my changes.
I believe the answer can be found between the lines of this post from Dieter:
The main problem [about __class__ switching] is that the class is usually coded (for efficiency reasons) in the persistent references to an object. As soon as the container is loaded from the ZODB, a ghost is created for its persistent reference using the class mentioned there.
This means: it is not safe to change the class of a persistent object unless you although modify all its containers (otherwise, some persistent references remain with the old class. This potentially leads to non-deterministic behaviour).
A workaround that seems working to me is to detach the object from the ZODB, switch the class and put the object back in place, like shown in here:
(pdb) obj = portal['example-1']
(pdb) del portal['example-1']
(pdb) obj
<Item at /Plone/example-1>
(pdb) from plone.dexterity.content import Container
(pdb) obj.__class__ = Container
(pdb) portal['example-1'] = obj
This is unorthodox for sure, but for the time being it proved to be a working solution without side effects.
Let's finish the job
Ok, now the object is a Container instance!
(pdb) obj
<Container at /Plone/example-1>
But this is instance is not yet ready. As David Glick suggests:
I doubt that [switching with the __class__] will be sufficient, since containers have internal datastructures that would need to get set up in order for the object tofunction as a container.
Infact visiting "Example 1" returns an error:
2013-02-20 16:03:23 ERROR Zope.SiteErrorLog 1361372603.770.0118314104451 http://localhost:8080/Plone/example-1
Traceback (innermost last):
Module ZPublisher.Publish, line 115, in publish
Module ZPublisher.BaseRequest, line 437, in traverse
Module Products.CMFCore.DynamicType, line 147, in __before_publishing_traverse__
Module Products.CMFDynamicViewFTI.fti, line 215, in queryMethodID
Module Products.CMFDynamicViewFTI.fti, line 182, in defaultView
Module Products.CMFPlone.PloneTool, line 847, in browserDefault
Module Products.CMFPlone.PloneTool, line 715, in getDefaultPage
Module Products.CMFPlone.utils, line 90, in getDefaultPage
Module plone.app.layout.navigation.defaultpage, line 32, in getDefaultPage
Module plone.app.layout.navigation.defaultpage, line 75, in getDefaultPage
Module plone.folder.ordered, line 202, in __contains__
TypeError: argument of type 'NoneType' is not iterable
Checking the code, it turns out that the _tree attribute of the instance is None as it can be verified with some analysis on the object:
(pdb) obj._tree is None
True
This is because we miss some initialization, so it's time to dive into Python and check which parent class __init__ function we have to call.
To understand this I analyzed the method resolution order of the two classes and tried to filtered out the parent classes that were not interesting for my sake, either because their __init__ is identical to object.__init__ or because they are in common with the Item class, and so their init method were already called during "Example 1" initialization.
A first attempt returns this:
(pdb) container_mro = Container.__mro__
(pdb) item_mro = Item.__mro__
(pdb) container_only_klasses = [klass for klass in container_mro if not klass in item_mro and not klass.__init__ is object.__init__]
(pdb) pp container_only_klasses
[<class 'plone.dexterity.content.Container'>,
<class 'plone.folder.ordered.CMFOrderedBTreeFolderBase'>,
<class 'plone.folder.ordered.OrderedBTreeFolderBase'>,
<class 'Products.BTreeFolder2.BTreeFolder2.BTreeFolder2Base'>,
<class 'Products.CMFCore.PortalFolder.PortalFolderBase'>,
<class 'OFS.Folder.Folder'>]
This considerably lowers the __init__ that we want to check. But we can go further, by noting that at the end it is enough to call the CMFOrderedBTreeFolderBase __init__method:
(pdb) from plone.folder.ordered import CMFOrderedBTreeFolderBase
(pdb) [klass for klass in container_only_klasses if not klass in CMFOrderedBTreeFolderBase.__mro__]
[<class 'plone.dexterity.content.Container'>]
Asking help on this class __init__ reveals:
(pdb) !help(CMFOrderedBTreeFolderBase.__init__)
Help on method __init__ in module plone.folder.ordered:
__init__(self, id, title='') unbound plone.folder.ordered.CMFOrderedBTreeFolderBase method
So to properly set up the missing blocks we just want to call this class __init__ passing as arguments the object itself, its id and its title:
(pdb) CMFOrderedBTreeFolderBase.__init__(obj, obj.getId(), obj.title)
Great, a new tree has born!
(pdb) obj._tree._tree <BTrees.OOBTree.OOBTree object at 0xb4a85f5c>
To finalize our work the object has to be reindexed and the transaction committed:
(pdb) obj.reindexObject()
(pdb) from transaction import commit
(pdb) commit()
Final remarks
Of course code like this is intended for an upgrade step!
The techniques described in this post are for sure interesting and powerful, but I would not rely on such low level tricks for routine operations, i.e. they are useful for doing migration or management scripts but definitely not suited for a browser view!
My use case was slightly different and more complicated: I was working with classes derived from the dexterity Item and Content classes, but I simplified it for making an already complex topic clearer.
If you have a more complicated case, further actions may be needed to complete properly this migration. In my case, for example, I also had to change the portal_type and remap some attributes.
Many times I read about the suggestion of creating content types that are folderish even if not requested by the specification. As you can see this choice allows more flexibility and can save you from the need of writing migration scripts or upgrade steps at a later time.
Credits
Original image adapted by @petraplatz