Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/configure.zcml
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/configure.zcml	(revision 1539)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/configure.zcml	(revision 1563)
@@ -124,4 +124,14 @@
         component=".interfacemanager.InterfacesImporterSection"
         name="quintagroup.transmogrifier.interfacesimporter"
+        />
+
+    <utility
+        component=".portlets.PortletsExporterSection"
+        name="quintagroup.transmogrifier.portletsexporter"
+        />
+
+    <utility
+        component=".portlets.PortletsImporterSection"
+        name="quintagroup.transmogrifier.portletsimporter"
         />
 
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/portlets.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/portlets.py	(revision 1563)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/portlets.py	(revision 1563)
@@ -0,0 +1,133 @@
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements, providedBy
+from zope.component import getUtilitiesFor, queryMultiAdapter
+
+from plone.portlets.interfaces import ILocalPortletAssignable, IPortletManager,\
+    IPortletAssignmentMapping
+from plone.portlets.constants import CONTEXT_CATEGORY
+from plone.app.portlets.interfaces import IPortletTypeInterface
+from plone.app.portlets.exportimport.interfaces import IPortletAssignmentExportImportHandler
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+class PortletsExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.doc = minidom.Document()
+
+    def __iter__(self):
+        self.portlet_schemata = dict([(iface, name,) for name, iface in 
+            getUtilitiesFor(IPortletTypeInterface)])
+        self.portlet_managers = getUtilitiesFor(IPortletManager)
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if ILocalPortletAssignable.providedBy(obj):
+                data = None
+
+                root = self.doc.createElement('portlets')
+                for elem in self.exportAssignments(obj):
+                    root.appendChild(elem)
+                #for elem in self.exportBlacklists(obj)
+                    #root.appendChild(elem)
+                if root.hasChildNodes():
+                    self.doc.appendChild(root)
+                    data = self.doc.toprettyxml(indent='  ', encoding='utf-8')
+                    self.doc.unlink()
+
+                if data:
+                    files = item.setdefault(self.fileskey, {})
+                    item[self.fileskey]['portlets'] = {
+                        'name': '.portlets.xml',
+                        'data': data,
+                    }
+            yield item
+
+    def exportAssignments(self, obj):
+        assignments = []
+        for manager_name, manager in self.portlet_managers:
+            mapping = queryMultiAdapter((obj, manager), IPortletAssignmentMapping)
+            mapping = mapping.__of__(obj)
+
+            for name, assignment in mapping.items():
+                type_ = None
+                for schema in providedBy(assignment).flattened():
+                    type_ = self.portlet_schemata.get(schema, None)
+                    if type_ is not None:
+                        break
+
+                if type_ is not None:
+                    child = self.doc.createElement('assignment')
+                    child.setAttribute('manager', manager_name)
+                    child.setAttribute('category', CONTEXT_CATEGORY)
+                    child.setAttribute('key', '/'.join(obj.getPhysicalPath()))
+                    child.setAttribute('type', type_)
+                    child.setAttribute('name', name)
+
+                    assignment = assignment.__of__(mapping)
+                    # use existing adapter for exporting a portlet assignment
+                    handler = IPortletAssignmentExportImportHandler(assignment)
+                    handler.export_assignment(schema, self.doc, child)
+
+                    assignments.append(child)
+
+        return assignments
+
+class PortletsImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+    def __iter__(self):
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+            if 'portlets' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            #if ILocalPortletAssignable.providedBy(obj):
+                #data = None
+                #data = item[fileskey]['portlets']['data']
+                #doc = minidom.parseString(data)
+                #root = doc.documentElement
+                #children = [k for k in root.childNodes]
+                #for child in children:
+                    #if child.nodeName != 'assignment':
+                        #continue
+                    #self.importPortlet(child)
+
+            yield item
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/portlets.txt
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/portlets.txt	(revision 1563)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/portlets.txt	(revision 1563)
@@ -0,0 +1,74 @@
+Portlets exporter and importer sections
+=======================================
+
+Portlets exporter and importer sections are used to migrate Plone 3 contextual 
+portlet assignments and blacklist settings.
+The portlets exporter section blueprint name is
+``quintagroup.transmogrifier.portletsexporter`` and importer section blueprint
+name is ``quintagroup.transmogrifier.portletsimporter``.
+
+Portlet assignments and blacklists are stored in XML format. Both sections need 
+relative path to the object with portlets assigned and importer also needs XML 
+data for updating them.
+
+Both sections has ``path-key`` option which specifies key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored.
+
+>>> import pprint
+>>> portlets = """
+... [transmogrifier]
+... pipeline =
+...     portletssource
+...     portletsexporter
+...     printer
+...     dataprinter
+...     portletsimporter
+...     
+... [portletssource]
+... blueprint = quintagroup.transmogrifier.tests.portletssource
+... 
+... [portletsexporter]
+... blueprint = quintagroup.transmogrifier.portletsexporter
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     portlets
+...     data
+... 
+... [portletsimporter]
+... blueprint = quintagroup.transmogrifier.portletsimporter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.portlets', portlets)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.portlets') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_path': 'not/existing/bar'}
+{'_path': 'spam/eggs/notassignable'}
+{'_files': {'portlets': {'data': ...
+                         'name': '.portlets.xml'}},
+ '_path': 'assignable'}
+<?xml version="1.0" encoding="utf-8"?>
+<portlets>
+  <assignment category="context" key="/assignable" manager="plone.leftcolumn" name="rss" type="portlets.rss">
+    <property name="count">
+      10
+    </property>
+    <property name="url">
+      http://sumno.com/feeds/main-page/
+    </property>
+    <property name="portlet_title">
+      RSS feed
+    </property>
+    <property name="timeout">
+      60
+    </property>
+  </assignment>
+</portlets>
+<BLANKLINE>
+>>> pprint.pprint(plone.updated)
+()
Index: quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/tests.py
===================================================================
--- quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/tests.py	(revision 1539)
+++ quintagroup.transmogrifier/trunk/quintagroup/transmogrifier/tests.py	(revision 1563)
@@ -57,5 +57,5 @@
 
     from Products.CMFCore import utils
-    def getToolByName(context, tool_id):
+    def getToolByName(context, tool_id, default=None):
         return context
     utils.getToolByName = getToolByName
@@ -1026,4 +1026,114 @@
         name=u'quintagroup.transmogrifier.tests.interfacemanagersource')
 
+def portletsSetUp(test):
+    sectionsSetUp(test)
+
+    from zope.interface import Interface
+    from zope.annotation.interfaces import IAttributeAnnotatable
+    from plone.portlets.interfaces import ILocalPortletAssignable
+
+    # this bases class adds __of__ method
+    from Acquisition import Implicit
+
+    class MockPortal(Implicit):
+        implements(IAttributeAnnotatable, ILocalPortletAssignable)
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notassignable'):
+                return object()
+            self._last_path = path
+            return self
+
+        def getPhysicalPath(self):
+            return [''] + self._last_path.split('/')
+
+        updated = ()
+        def _updateProperty(self, id, value):
+            self.updated += ((self._last_path, id, value))
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class PortletsSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(PortletsSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notassignable'),
+                dict(_path='assignable'),
+            )
+
+    provideUtility(PortletsSource,
+        name=u'quintagroup.transmogrifier.tests.portletssource')
+
+    # prepare the one portlet for testing
+    from zope.interface import alsoProvides
+    from zope.component import getUtility, getMultiAdapter
+    from zope.component.interfaces import IFactory
+    from zope.component.factory import Factory
+
+    from plone.portlets.manager import PortletManager
+    from plone.portlets.interfaces import IPortletManager, IPortletType, \
+        IPortletAssignmentMapping
+    from plone.portlets.registration import PortletType
+
+    from plone.app.portlets.assignable import localPortletAssignmentMappingAdapter
+    from plone.app.portlets.interfaces import IPortletTypeInterface
+    # from plone.app.portlets.browser.interfaces import IPortletAdding
+    from plone.app.portlets.portlets.rss import IRSSPortlet, Assignment #, Renderer, AddForm, EditForm
+
+    # register portlet manager and assignment mapping adapter
+    manager = PortletManager()
+    provideUtility(manager, IPortletManager, name='plone.leftcolumn')
+    provideAdapter(localPortletAssignmentMappingAdapter)
+    mapping = getMultiAdapter((portal, manager), IPortletAssignmentMapping)
+
+    # register portlet (this is what plone:portlet zcml directive does)
+    PORTLET_NAME = 'portlets.rss'
+    alsoProvides(IRSSPortlet, IPortletTypeInterface)
+    provideUtility(provides=IPortletTypeInterface, name=PORTLET_NAME, component=IRSSPortlet)
+    provideUtility(provides=IFactory, name=PORTLET_NAME, component=Factory(Assignment))
+
+    # register a portlet type (this is what <portlet /> element in portlets.xml
+    # does)
+    portlet = PortletType()
+    portlet.title = 'RSS Feed'
+    portlet.description = 'A portlet which can receive and render an RSS feed.'
+    portlet.addview = PORTLET_NAME
+    portlet.for_ = [Interface]
+    provideUtility(component=portlet, provides=IPortletType, 
+        name=PORTLET_NAME)
+
+    # add a portlet and configure it (this is done on @@manage-portlets view)
+    assignment = getUtility(IFactory, name=PORTLET_NAME)()
+    mapping['rss'] = assignment
+    portlet_interface = getUtility(IPortletTypeInterface, name=PORTLET_NAME)
+    data = {
+        'portlet_title': u'RSS feed',
+        'count': 10,
+        'url': u'http://sumno.com/feeds/main-page/',
+        'timeout': 60
+    }
+    for k, v in data.items():
+        field = portlet_interface.get(k)
+        field = field.bind(assignment)
+        field.validate(v)
+        field.set(assignment, v)
+
+    # register adapter for portlet assignment export/import
+    from plone.app.portlets.exportimport.portlets import PropertyPortletAssignmentExportImportHandler
+    provideAdapter(PropertyPortletAssignmentExportImportHandler)
 
 def test_suite():
@@ -1073,4 +1183,7 @@
             'interfacemanager.txt',
             setUp=interfaceManagerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'portlets.txt',
+            setUp=portletsSetUp, tearDown=tearDown),
     ))
     return suite
