Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-02T14:12:48+01:00
Author: Johannes Raggam (thet) <raggam-nl@adm.at>
Commit: https://github.com/plone/plone.app.contenttypes/commit/880fc6f7d6517c7c575206367bed7f5ea0457e37

[#18] Add folderish content classes and FTI definitions for News Item, Document and Event

Files changed:
A plone/app/contenttypes/profiles/folderish/metadata.xml
A plone/app/contenttypes/profiles/folderish/types.xml
A plone/app/contenttypes/profiles/folderish/types/Document.xml
A plone/app/contenttypes/profiles/folderish/types/Event.xml
A plone/app/contenttypes/profiles/folderish/types/News_Item.xml
M plone/app/contenttypes/content.py
M plone/app/contenttypes/profiles.zcml

diff --git a/plone/app/contenttypes/content.py b/plone/app/contenttypes/content.py
index 5b8d4e5..dcd673f 100644
--- a/plone/app/contenttypes/content.py
+++ b/plone/app/contenttypes/content.py
@@ -95,3 +95,21 @@ class NewsItem(Item):
 class Event(Item):
     """Convinience subclass for ``File`` portal type
     """
+
+
+@implementer(IDocument)
+class FolderishDocument(Container):
+    """Convinience subclass for ``File`` portal type
+    """
+
+
+@implementer(IEvent)
+class FolderishEvent(Container):
+    """Convinience subclass for ``File`` portal type
+    """
+
+
+@implementer(INewsItem)
+class FolderishNewsItem(Container):
+    """Convinience subclass for ``File`` portal type
+    """
diff --git a/plone/app/contenttypes/profiles.zcml b/plone/app/contenttypes/profiles.zcml
index 886bd84..b2a315e 100644
--- a/plone/app/contenttypes/profiles.zcml
+++ b/plone/app/contenttypes/profiles.zcml
@@ -20,6 +20,16 @@
     provides="Products.GenericSetup.interfaces.EXTENSION"
     />
 
+  <!-- All types, News Item, Document and Event folderish, no sample content,
+       depends on default -->
+  <gs:registerProfile
+    name="folderish"
+    title="Plone default content-types (Folderish behavior) - dexterity"
+    directory="profiles/folderish"
+    description="This provides Plones default types without adding example comntent"
+    provides="Products.GenericSetup.interfaces.EXTENSION"
+    />
+
   <!-- Uninstall -->
   <gs:registerProfile
     name="uninstall"
diff --git a/plone/app/contenttypes/profiles/folderish/metadata.xml b/plone/app/contenttypes/profiles/folderish/metadata.xml
new file mode 100644
index 0000000..743d12c
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/metadata.xml
@@ -0,0 +1,6 @@
+<metadata>
+ <version>1</version>
+ <dependencies>
+  <dependency>profile-plone.app.contenttypes:default</dependency>
+ </dependencies>
+</metadata>
diff --git a/plone/app/contenttypes/profiles/folderish/types.xml b/plone/app/contenttypes/profiles/folderish/types.xml
new file mode 100644
index 0000000..34e8b69
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object meta_type="Plone Types Tool" name="portal_types">
+ <object meta_type="Dexterity FTI" name="Document" />
+ <object meta_type="Dexterity FTI" name="News Item" />
+ <object meta_type="Dexterity FTI" name="Event" />
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Document.xml b/plone/app/contenttypes/profiles/folderish/types/Document.xml
new file mode 100644
index 0000000..6c997f2
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Document.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Document" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishDocument</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Event.xml b/plone/app/contenttypes/profiles/folderish/types/Event.xml
new file mode 100644
index 0000000..710f9a9
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Event.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Event" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishEvent</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/News_Item.xml b/plone/app/contenttypes/profiles/folderish/types/News_Item.xml
new file mode 100644
index 0000000..c1a8a30
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/News_Item.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="News Item" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishNewsItem</property> 
+</object>


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-02T14:13:02+01:00
Author: Bogdan Girman (bogdangi) <bogdan.girman@gmail.com>
Commit: https://github.com/plone/plone.app.contenttypes/commit/f992fd215fcd7b6a4ea3592ad370ec42b6efe705

[#18] Add helpers to migrate object with changed base class

Files changed:
M plone/app/contenttypes/migration/browser.py
M plone/app/contenttypes/migration/configure.zcml
M plone/app/contenttypes/migration/dxmigration.py
M plone/app/contenttypes/migration/vocabularies.py
M plone/app/contenttypes/tests/test_migration.py

diff --git a/plone/app/contenttypes/migration/browser.py b/plone/app/contenttypes/migration/browser.py
index 1e94d1f..5d13741 100644
--- a/plone/app/contenttypes/migration/browser.py
+++ b/plone/app/contenttypes/migration/browser.py
@@ -10,6 +10,7 @@
 from datetime import datetime
 from datetime import timedelta
 from plone.app.contenttypes.migration import migration
+from plone.app.contenttypes.migration import dxmigration
 from plone.app.contenttypes.migration.utils import HAS_MULTILINGUAL
 from plone.app.contenttypes.migration.utils import installTypeIfNeeded
 from plone.app.contenttypes.migration.utils import isSchemaExtended
@@ -362,6 +363,65 @@ def updateWidgets(self):
 )
 
 
+class IBaseClassMigratorForm(Interface):
+
+    changed_base_classes = schema.List(
+        title=u'Changed base classes',
+        description=u'Select changed base classes you want to migrate',
+        value_type=schema.Choice(
+            vocabulary='plone.app.contenttypes.migration.changed_base_classes',
+        ),
+        required=True,
+    )
+
+
+class BaseClassMigratorForm(form.Form):
+
+    fields = field.Fields(IBaseClassMigratorForm)
+    fields['changed_base_classes'].widgetFactory = CheckBoxFieldWidget
+    ignoreContext = True
+    enableCSRFProtection = True
+
+    @button.buttonAndHandler(u'Update', name='update')
+    def handle_migrate(self, action):
+        data, errors = self.extractData()
+
+        if errors:
+            return
+
+        changed_base_classes = data.get('changed_base_classes', [])
+        if not changed_base_classes:
+            return
+
+        catalog = getToolByName(self.context, "portal_catalog")
+        migrated = []
+        not_migrated = []
+        for brain in catalog():
+            obj = brain.getObject()
+            old_class_name = dxmigration.get_old_class_name_string(obj)
+            if old_class_name in changed_base_classes:
+                if dxmigration.migrate_base_class_to_new_class(obj):
+                    migrated.append(obj)
+                else:
+                    not_migrated.append(obj)
+
+        messages = IStatusMessage(self.request)
+        info_message_template = 'There are {0} objects migrated.'
+        warn_message_template = 'There are not {0} objects migrated.'
+        if migrated:
+            msg = info_message_template.format(len(migrated))
+            messages.addStatusMessage(msg, type='info')
+        if not_migrated:
+            msg = warn_message_template.format(len(not_migrated))
+            messages.addStatusMessage(msg, type='warn')
+        self.request.response.redirect(self.request['ACTUAL_URL'])
+
+
+BaseClassMigrator = wrap_form(
+    BaseClassMigratorForm,
+)
+
+
 class ATCTMigratorHelpers(BrowserView):
 
     def objects_to_be_migrated(self):
diff --git a/plone/app/contenttypes/migration/configure.zcml b/plone/app/contenttypes/migration/configure.zcml
index e912337..9881860 100644
--- a/plone/app/contenttypes/migration/configure.zcml
+++ b/plone/app/contenttypes/migration/configure.zcml
@@ -36,6 +36,14 @@
     />
 
   <browser:page
+    name="base_class_migrator_form"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    class=".browser.BaseClassMigrator"
+    layer="plone.app.contenttypes.interfaces.IPloneAppContenttypesLayer"
+    permission="cmf.ManagePortal"
+    />
+
+  <browser:page
     name="atct_migrator_helpers"
     for="Products.CMFPlone.interfaces.IPloneSiteRoot"
     class=".browser.ATCTMigratorHelpers"
@@ -69,6 +77,11 @@
       name="plone.app.contenttypes.migration.extendedtypes"
       provides="zope.schema.interfaces.IVocabularyFactory" />
 
+  <utility
+      factory=".vocabularies.ChangedBaseClasses"
+      name="plone.app.contenttypes.migration.changed_base_classes"
+      provides="zope.schema.interfaces.IVocabularyFactory" />
+
   <adapter name="nullmigrator" factory=".migration.BaseCustomMigator"/>
 
 </configure>
diff --git a/plone/app/contenttypes/migration/dxmigration.py b/plone/app/contenttypes/migration/dxmigration.py
index b5ce49a..09d1a0b 100644
--- a/plone/app/contenttypes/migration/dxmigration.py
+++ b/plone/app/contenttypes/migration/dxmigration.py
@@ -1,11 +1,16 @@
 # -*- coding: utf-8 -*-
+from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2Base
+from Products.CMFCore.utils import getToolByName
 from Products.contentmigration.basemigrator.migrator import CMFItemMigrator
 from Products.contentmigration.basemigrator.walker import CatalogWalker
 from plone.app.contenttypes.interfaces import IEvent
 from plone.app.contenttypes.migration import datetime_fixer
+from plone.dexterity.content import Container
+from plone.dexterity.content import Item
 from plone.event.utils import default_timezone
 from zope.annotation.interfaces import IAnnotations
 from zope.component.hooks import getSite
+import importlib
 
 
 def migrate(portal, migrator):
@@ -100,3 +105,80 @@ def migrate_schema_fields(self):
         old_text = annotations.get(
             'plone.app.event.dx.behaviors.IEventSummary.text', None)
         self.new.text = old_text
+
+
+def get_old_class_name_string(obj):
+    """Returns old class name string."""
+    return '{0}.{1}'.format(obj.__module__, obj.__class__.__name__)
+
+
+def get_portal_type_name_string(obj):
+    portal = getSite()
+    types = getToolByName(portal, "portal_types")
+    portal_type = types.get(obj.portal_type)
+    if not portal_type:
+        return
+
+    return portal_type.klass
+
+
+def migrate_base_class_to_new_class(obj,
+                                    indexes=[
+                                        'is_folderish',
+                                        'object_provides',
+                                    ],
+                                    ):
+    new_class_name = get_portal_type_name_string(obj)
+    current_class_name = get_old_class_name_string(obj)
+
+    if new_class_name == current_class_name:
+        return False
+
+    was_item = isinstance(obj, Item)
+    obj_id = obj.getId()
+    module_name, class_name = new_class_name.rsplit('.', 1)
+    module = importlib.import_module(module_name)
+    new_class = getattr(module, class_name)
+
+    # update obj class
+    parent = obj.__parent__
+    parent._delOb(obj_id)
+    obj.__class__ = new_class
+    parent._setOb(obj_id, obj)
+
+    is_container = isinstance(obj, Container)
+
+    # If object was Item likesh and becomes Containerish we have to do few
+    # preparation
+    if was_item and is_container:
+        # update obj _tree, because now it is container
+        BTreeFolder2Base._initBTrees(obj)
+
+    # reindex
+    obj.reindexObject(indexes)
+
+    return True
+
+
+def list_of_objects_with_changed_base_class():
+    portal = getSite()
+
+    catalog = getToolByName(portal, "portal_catalog")
+
+    for brain in catalog():
+        obj = brain.getObject()
+        if get_portal_type_name_string(obj) != get_old_class_name_string(obj):
+            yield obj
+
+
+def list_of_changed_base_class_names():
+    """Returns list of class names that are not longer in portal_types."""
+    changed_base_class_names = {}
+    for obj in list_of_objects_with_changed_base_class():
+        changed_base_class_name = get_old_class_name_string(obj)
+        if changed_base_class_name not in changed_base_class_names:
+            number_objects = changed_base_class_names.get(
+                changed_base_class_name, 0)
+            changed_base_class_names[changed_base_class_name] = \
+                number_objects + 1
+    return changed_base_class_names
diff --git a/plone/app/contenttypes/migration/vocabularies.py b/plone/app/contenttypes/migration/vocabularies.py
index 8f46aa5..f6d710c 100644
--- a/plone/app/contenttypes/migration/vocabularies.py
+++ b/plone/app/contenttypes/migration/vocabularies.py
@@ -204,3 +204,19 @@ def __call__(self, context):
         extended fields.
         """
         return results(context, show_extended=True)
+
+
+class ChangedBaseClasses(object):
+    implements(IVocabularyFactory)
+
+    def __call__(self, context):
+        """Return a vocabulary with all changed base classes."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_changed_base_class_names
+        list_of_class_names = list_of_changed_base_class_names() or {}
+        return SimpleVocabulary(
+            [SimpleVocabulary.createTerm(
+                class_name, class_name,
+                '{0} ({1})'.format(class_name, list_of_class_names[class_name]))
+             for class_name in list_of_class_names.keys()]
+        )
diff --git a/plone/app/contenttypes/tests/test_migration.py b/plone/app/contenttypes/tests/test_migration.py
index 76e1ff8..ab3823a 100644
--- a/plone/app/contenttypes/tests/test_migration.py
+++ b/plone/app/contenttypes/tests/test_migration.py
@@ -2,12 +2,19 @@
 from Products.CMFCore.utils import getToolByName
 from five.intid.intid import IntIds
 from five.intid.site import addUtility
+from lxml import etree
+from plone.app.contenttypes.testing import \
+    PLONE_APP_CONTENTTYPES_FUNCTIONAL_TESTING
 from plone.app.contenttypes.testing import \
     PLONE_APP_CONTENTTYPES_MIGRATION_TESTING
 from plone.app.contenttypes.testing import set_browserlayer
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
 from plone.app.testing import applyProfile
 from plone.app.testing import login
+from plone.dexterity.content import Container
 from plone.event.interfaces import IEventAccessor
+from plone.testing.z2 import Browser
 from zope.annotation.interfaces import IAnnotations
 from zope.component import getMultiAdapter
 from zope.component import getSiteManager
@@ -1280,3 +1287,125 @@ def test_migration_view_confirmation(self):
         )
         results = migration_view()
         self.assertIn('@@migrate_from_atct?migrate=1', results)
+
+
+class MigrateDexterityBaseClassIntegrationTest(unittest.TestCase):
+
+    layer = PLONE_APP_CONTENTTYPES_MIGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+
+        applyProfile(self.portal, 'plone.app.dexterity:testing')
+
+        self.portal.acl_users.userFolderAddUser('admin',
+                                                'secret',
+                                                ['Manager'],
+                                                [])
+        login(self.portal, 'admin')
+
+        # Add default content
+        self.portal.invokeFactory('Document', 'item')
+
+        # Change Document conent type to folderish
+        portal_types = getToolByName(self.portal, 'portal_types')
+        portal_types['Document'].klass = 'plone.dexterity.content.Container'
+        portal_types['Document'].allowed_content_types = ('Document',)
+
+    def test_dxmigration_migrate_item_to_container_class_is_changed(self):
+        """Check that base class was changed."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            migrate_base_class_to_new_class
+        migrate_base_class_to_new_class(self.portal.item)
+        self.assertTrue(isinstance(self.portal.item, Container))
+
+    def test_dxmigration_migrate_item_to_container_add_object_inside(self):
+        """Check that after migrate base class it can add items inside object.
+        """
+        from plone.app.contenttypes.migration.dxmigration import \
+            migrate_base_class_to_new_class
+        migrate_base_class_to_new_class(self.portal.item)
+        self.portal.item.invokeFactory('Document', 'doc')
+        self.assertEqual(len(self.portal.item.folderlistingFolderContents()), 1)
+
+    def test_dxmigration_migrate_list_of_objects_with_changed_base_class(self):
+        """Check list of objects with changed classes."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_objects_with_changed_base_class
+        # We have already one changed object
+        objects = [i for i in list_of_objects_with_changed_base_class()]
+        self.assertEqual(len(objects), 1)
+
+    def test_dxmigration_migrate_list_of_changed_base_class_names(self):
+        """Check list of changed base class names."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_changed_base_class_names
+        # We have already one changed object
+        names = [i for i in list_of_changed_base_class_names()]
+        self.assertEqual(len(names), 1)
+
+    def test_dxmigration_migrate_vocabulary_changed_base_classes(self):
+        """Check vocabulary of changed base class names."""
+        # We have already one changed object
+        name = 'plone.app.contenttypes.migration.changed_base_classes'
+        factory = getUtility(IVocabularyFactory, name)
+        vocabulary = factory(self.portal)
+        self.assertEqual(len(vocabulary), 1)
+
+
+class MigrateDexterityBaseClassFunctionalTest(unittest.TestCase):
+
+    layer = PLONE_APP_CONTENTTYPES_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+
+        self.portal_url = self.portal.absolute_url()
+        self.manage_document_url = '{0}/{1}/{2}/{3}'.format(
+            self.portal_url,
+            'portal_types',
+            'Document',
+            'manage_propertiesForm',
+        )
+
+        self.browser = Browser(app)
+        self.browser.handleErrors = False
+        self.browser.addHeader(
+            'Authorization',
+            'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
+        )
+
+        # Add default content
+        self.browser.open(self.portal_url)
+        self.browser.getLink('Page').click()
+        self.browser.getControl(name='form.widgets.IDublinCore.title')\
+            .value = "My item"
+        self.browser.getControl(name='form.widgets.IShortName.id')\
+            .value = "item"
+        self.browser.getControl('Save').click()
+
+        # Change Document conent type to folderish
+        self.browser.open(self.manage_document_url)
+        self.browser.getControl(name='klass:string') \
+            .value = 'plone.app.contenttypes.content.Collection'
+        self.browser.getControl('Save Changes').click()
+        self.browser.open(
+            '{0}/@@base_class_migrator_form'.format(self.portal_url))
+        self.good_info_message_template = 'There are {0} objects migrated.'
+
+    def test_dxmigration_migrate_check_migration_form_view(self):
+        """Check base class migrator view of changed base class names."""
+        html = etree.HTML(self.browser.contents)
+        checkboxes = html.xpath('//form//*[@name="{0}"]'.format(
+            'form.widgets.changed_base_classes:list'))
+        self.assertEqual(len(checkboxes), 1)
+
+    def test_dxmigration_migrate_check_migration_successful_message(self):
+        """Check base class migrator view of changed base class names."""
+        self.browser.getControl(name='form.widgets.changed_base_classes:list') \
+            .value = ['true']
+        self.browser.getControl('Update').click()
+        self.assertIn(
+            self.good_info_message_template.format(1), self.browser.contents)


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-02T15:13:32+01:00
Author: Bogdan Girman (bogdangi) <bogdan.girman@gmail.com>
Commit: https://github.com/plone/plone.app.contenttypes/commit/c906880b28aeeb420a86e4180290e247782a9058

[#18] Add documentation to migrate all to folderish content type

Files changed:
M docs/README.rst

diff --git a/docs/README.rst b/docs/README.rst
index 240807e..a4fb850 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -218,6 +218,36 @@ Future versions of plone.app.contenttypes will have with a form that allows you
 However if you'd like to migrate your content-types to Dexterity before this feature is completed you might want to have a look at the code of plone.app.contenttypes.migration.migration.NewsItemMigrator as a blueprint for a migration.
 
 
+Migrating non folderish to folderish objects (or change base class for any dexterety base type)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Migration non folderish object to folderish has few steps.
+
+Step 1 (Update base content types folderish)
+-------------------------------------------
+
+Apply `folderish` profile.
+
+- Go to portal_setup->Import;
+
+- Select `Plone default content-types (Folderish behavior) - dexterity`
+
+- Click  `Import all steps`
+
+
+Step 2 (Update base class for exist objects)
+--------------------------------------------
+
+There is update changed base class migration step which allow you to update base class if last one was changed.
+
+To get this migration form you have to open follow link `PORTAL_URL/@@base_class_migrator_form`.
+
+On this view you will see check boxes with class name string and numbers of changed objects that are going to be updated.
+Select classes that you want to update and click on button `Update`.
+
+If migration was successful you see the info box with number of successfully updated objects or warning with number of not updated objects.
+
+
 Widgets
 -------
 


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:31:31+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/54f20c5e13228f46ec8b10c3b2a906c4ed8ab8cf

allow folderish collections

Files changed:
A plone/app/contenttypes/profiles/folderish/types/Collection.xml
M plone/app/contenttypes/content.py
M plone/app/contenttypes/profiles/folderish/types.xml

diff --git a/plone/app/contenttypes/content.py b/plone/app/contenttypes/content.py
index dcd673f..07b366d 100644
--- a/plone/app/contenttypes/content.py
+++ b/plone/app/contenttypes/content.py
@@ -14,6 +14,8 @@
 
 @implementer(ICollection)
 class Collection(Item):
+    """Convinience Item subclass for ``Collection`` portal type
+    """
     # BBB
 
     def listMetaDataFields(self, exclude=True):
@@ -69,47 +71,54 @@ class File(Item):
 
 @implementer(IFolder)
 class Folder(Container):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Folder`` portal type
     """
 
 
 @implementer(IImage)
 class Image(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Image`` portal type
     """
 
 
 @implementer(ILink)
 class Link(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Link`` portal type
     """
 
 
 @implementer(INewsItem)
 class NewsItem(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``News Item`` portal type
     """
 
 
 @implementer(IEvent)
 class Event(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Event`` portal type
+    """
+
+# Folderish subclasses used by 'profile-plone.app.contenttypes:folderish'
+
+@implementer(ICollection)
+class FolderishCollection(Container):
+    """Folderish convinience subclass for ``Collection`` portal type
     """
 
 
 @implementer(IDocument)
 class FolderishDocument(Container):
-    """Convinience subclass for ``File`` portal type
+    """Folderish convinience subclass for ``Document`` portal type
     """
 
 
 @implementer(IEvent)
 class FolderishEvent(Container):
-    """Convinience subclass for ``File`` portal type
+    """Folderish convinience subclass for ``Event`` portal type
     """
 
 
 @implementer(INewsItem)
 class FolderishNewsItem(Container):
-    """Convinience subclass for ``File`` portal type
+    """Folderish convinience subclass for ``News Item`` portal type
     """
diff --git a/plone/app/contenttypes/profiles/folderish/types.xml b/plone/app/contenttypes/profiles/folderish/types.xml
index 34e8b69..740c1e8 100644
--- a/plone/app/contenttypes/profiles/folderish/types.xml
+++ b/plone/app/contenttypes/profiles/folderish/types.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0"?>
 <object meta_type="Plone Types Tool" name="portal_types">
+ <object meta_type="Dexterity FTI" name="Collection" />
  <object meta_type="Dexterity FTI" name="Document" />
  <object meta_type="Dexterity FTI" name="News Item" />
  <object meta_type="Dexterity FTI" name="Event" />
diff --git a/plone/app/contenttypes/profiles/folderish/types/Collection.xml b/plone/app/contenttypes/profiles/folderish/types/Collection.xml
new file mode 100644
index 0000000..a396a48
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Collection.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Collection" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishCollection</property>
+</object>


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:31:42+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/627de331e528ed5a5b2278348676e6f485890291

update documentation

Files changed:
M docs/README.rst

diff --git a/docs/README.rst b/docs/README.rst
index a4fb850..b2ea4ab 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -218,35 +218,27 @@ Future versions of plone.app.contenttypes will have with a form that allows you
 However if you'd like to migrate your content-types to Dexterity before this feature is completed you might want to have a look at the code of plone.app.contenttypes.migration.migration.NewsItemMigrator as a blueprint for a migration.
 
 
-Migrating non folderish to folderish objects (or change base class for any dexterety base type)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Migrating to folderish objects or change base class for any dexterity type
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 Migration non folderish object to folderish has few steps.
 
-Step 1 (Update base content types folderish)
--------------------------------------------
-
-Apply `folderish` profile.
-
-- Go to portal_setup->Import;
+Step 1: Enable folderish types
+++++++++++++++++++++++++++++++
 
-- Select `Plone default content-types (Folderish behavior) - dexterity`
-
-- Click  `Import all steps`
+See `Using folderish types`_
 
 
-Step 2 (Update base class for exist objects)
---------------------------------------------
+Step 2: Update base class for existing objects
+++++++++++++++++++++++++++++++++++++++++++++++
 
-There is update changed base class migration step which allow you to update base class if last one was changed.
+If you changed the base-class of existing types you might also want to upgrade the base-class of existing objects. You can use the following form for this: `PORTAL_URL/@@base_class_migrator_form`.
 
-To get this migration form you have to open follow link `PORTAL_URL/@@base_class_migrator_form`.
+On this you will see checkboxes with class names and the amount of objects that are going to be updated. Select classes that you want to update and click on button `Update`.
 
-On this view you will see check boxes with class name string and numbers of changed objects that are going to be updated.
-Select classes that you want to update and click on button `Update`.
-
-If migration was successful you see the info box with number of successfully updated objects or warning with number of not updated objects.
+If the migration was successful you see the info box with number of successfully updated objects or a warning with number of not updated objects.
 
+This form can be used to change the base-class of any dexterity-types instances.
 
 Widgets
 -------
@@ -323,6 +315,29 @@ If you want to add plone.app.contenttypes as a dependency from another products
 If you use the profile ``default`` then the default-content in new sites will still be Archetypes-based. You'll then have to migrate that content using the migration-form ``@@atct_migrator`` or delete it by hand.
 
 
+Using folderish types
+^^^^^^^^^^^^^^^^^^^^^
+
+If you want all types folderish you need to depend on the profile ``folderish`` in your own ``metadata.xml``.
+
+.. code:: xml
+
+    <metadata>
+      <version>1</version>
+        <dependencies>
+            <dependency>profile-plone.app.contenttypes:folderish</dependency>
+        </dependencies>
+    </metadata>
+
+You can also enable this profile by hand by applying the `folderish` profile:
+
+- Go to portal_setup->Import;
+- Select `Plone default content-types (Folderish behavior) - dexterity`
+- Click  `Import all steps`
+
+You can also migrate your existing objects to folderish types. For details see `Step 2: Update base class for existing objects`_
+
+
 Extending the types
 ^^^^^^^^^^^^^^^^^^^
 


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:33:15+01:00
Author: Johannes Raggam (thet) <raggam-nl@adm.at>
Commit: https://github.com/plone/plone.app.contenttypes/commit/acfb8a1db1463c11b56d990ecafb6196f458147e

[#18] Add folderish content classes and FTI definitions for News Item, Document and Event

Files changed:
A plone/app/contenttypes/profiles/folderish/metadata.xml
A plone/app/contenttypes/profiles/folderish/types.xml
A plone/app/contenttypes/profiles/folderish/types/Document.xml
A plone/app/contenttypes/profiles/folderish/types/Event.xml
A plone/app/contenttypes/profiles/folderish/types/News_Item.xml
M plone/app/contenttypes/content.py
M plone/app/contenttypes/profiles.zcml

diff --git a/plone/app/contenttypes/content.py b/plone/app/contenttypes/content.py
index 5b8d4e5..dcd673f 100644
--- a/plone/app/contenttypes/content.py
+++ b/plone/app/contenttypes/content.py
@@ -95,3 +95,21 @@ class NewsItem(Item):
 class Event(Item):
     """Convinience subclass for ``File`` portal type
     """
+
+
+@implementer(IDocument)
+class FolderishDocument(Container):
+    """Convinience subclass for ``File`` portal type
+    """
+
+
+@implementer(IEvent)
+class FolderishEvent(Container):
+    """Convinience subclass for ``File`` portal type
+    """
+
+
+@implementer(INewsItem)
+class FolderishNewsItem(Container):
+    """Convinience subclass for ``File`` portal type
+    """
diff --git a/plone/app/contenttypes/profiles.zcml b/plone/app/contenttypes/profiles.zcml
index 886bd84..b2a315e 100644
--- a/plone/app/contenttypes/profiles.zcml
+++ b/plone/app/contenttypes/profiles.zcml
@@ -20,6 +20,16 @@
     provides="Products.GenericSetup.interfaces.EXTENSION"
     />
 
+  <!-- All types, News Item, Document and Event folderish, no sample content,
+       depends on default -->
+  <gs:registerProfile
+    name="folderish"
+    title="Plone default content-types (Folderish behavior) - dexterity"
+    directory="profiles/folderish"
+    description="This provides Plones default types without adding example comntent"
+    provides="Products.GenericSetup.interfaces.EXTENSION"
+    />
+
   <!-- Uninstall -->
   <gs:registerProfile
     name="uninstall"
diff --git a/plone/app/contenttypes/profiles/folderish/metadata.xml b/plone/app/contenttypes/profiles/folderish/metadata.xml
new file mode 100644
index 0000000..743d12c
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/metadata.xml
@@ -0,0 +1,6 @@
+<metadata>
+ <version>1</version>
+ <dependencies>
+  <dependency>profile-plone.app.contenttypes:default</dependency>
+ </dependencies>
+</metadata>
diff --git a/plone/app/contenttypes/profiles/folderish/types.xml b/plone/app/contenttypes/profiles/folderish/types.xml
new file mode 100644
index 0000000..34e8b69
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object meta_type="Plone Types Tool" name="portal_types">
+ <object meta_type="Dexterity FTI" name="Document" />
+ <object meta_type="Dexterity FTI" name="News Item" />
+ <object meta_type="Dexterity FTI" name="Event" />
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Document.xml b/plone/app/contenttypes/profiles/folderish/types/Document.xml
new file mode 100644
index 0000000..6c997f2
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Document.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Document" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishDocument</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Event.xml b/plone/app/contenttypes/profiles/folderish/types/Event.xml
new file mode 100644
index 0000000..710f9a9
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Event.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Event" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishEvent</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/News_Item.xml b/plone/app/contenttypes/profiles/folderish/types/News_Item.xml
new file mode 100644
index 0000000..c1a8a30
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/News_Item.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="News Item" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishNewsItem</property> 
+</object>


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:34:39+01:00
Author: Bogdan Girman (bogdangi) <bogdan.girman@gmail.com>
Commit: https://github.com/plone/plone.app.contenttypes/commit/2d8c5fe6ebe79f9d49b1da0a469c1cc2d554b46c

[#18] Add helpers to migrate object with changed base class

Files changed:
M plone/app/contenttypes/migration/browser.py
M plone/app/contenttypes/migration/configure.zcml
M plone/app/contenttypes/migration/dxmigration.py
M plone/app/contenttypes/migration/vocabularies.py
M plone/app/contenttypes/tests/test_migration.py

diff --git a/plone/app/contenttypes/migration/browser.py b/plone/app/contenttypes/migration/browser.py
index 841b6d3..112481f 100644
--- a/plone/app/contenttypes/migration/browser.py
+++ b/plone/app/contenttypes/migration/browser.py
@@ -10,6 +10,7 @@
 from datetime import datetime
 from datetime import timedelta
 from plone.app.contenttypes.migration import migration
+from plone.app.contenttypes.migration import dxmigration
 from plone.app.contenttypes.migration.utils import HAS_MULTILINGUAL
 from plone.app.contenttypes.migration.utils import installTypeIfNeeded
 from plone.app.contenttypes.migration.utils import isSchemaExtended
@@ -362,6 +363,65 @@ def updateWidgets(self):
 )
 
 
+class IBaseClassMigratorForm(Interface):
+
+    changed_base_classes = schema.List(
+        title=u'Changed base classes',
+        description=u'Select changed base classes you want to migrate',
+        value_type=schema.Choice(
+            vocabulary='plone.app.contenttypes.migration.changed_base_classes',
+        ),
+        required=True,
+    )
+
+
+class BaseClassMigratorForm(form.Form):
+
+    fields = field.Fields(IBaseClassMigratorForm)
+    fields['changed_base_classes'].widgetFactory = CheckBoxFieldWidget
+    ignoreContext = True
+    enableCSRFProtection = True
+
+    @button.buttonAndHandler(u'Update', name='update')
+    def handle_migrate(self, action):
+        data, errors = self.extractData()
+
+        if errors:
+            return
+
+        changed_base_classes = data.get('changed_base_classes', [])
+        if not changed_base_classes:
+            return
+
+        catalog = getToolByName(self.context, "portal_catalog")
+        migrated = []
+        not_migrated = []
+        for brain in catalog():
+            obj = brain.getObject()
+            old_class_name = dxmigration.get_old_class_name_string(obj)
+            if old_class_name in changed_base_classes:
+                if dxmigration.migrate_base_class_to_new_class(obj):
+                    migrated.append(obj)
+                else:
+                    not_migrated.append(obj)
+
+        messages = IStatusMessage(self.request)
+        info_message_template = 'There are {0} objects migrated.'
+        warn_message_template = 'There are not {0} objects migrated.'
+        if migrated:
+            msg = info_message_template.format(len(migrated))
+            messages.addStatusMessage(msg, type='info')
+        if not_migrated:
+            msg = warn_message_template.format(len(not_migrated))
+            messages.addStatusMessage(msg, type='warn')
+        self.request.response.redirect(self.request['ACTUAL_URL'])
+
+
+BaseClassMigrator = wrap_form(
+    BaseClassMigratorForm,
+)
+
+
 class ATCTMigratorHelpers(BrowserView):
 
     def objects_to_be_migrated(self):
diff --git a/plone/app/contenttypes/migration/configure.zcml b/plone/app/contenttypes/migration/configure.zcml
index e912337..9881860 100644
--- a/plone/app/contenttypes/migration/configure.zcml
+++ b/plone/app/contenttypes/migration/configure.zcml
@@ -36,6 +36,14 @@
     />
 
   <browser:page
+    name="base_class_migrator_form"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    class=".browser.BaseClassMigrator"
+    layer="plone.app.contenttypes.interfaces.IPloneAppContenttypesLayer"
+    permission="cmf.ManagePortal"
+    />
+
+  <browser:page
     name="atct_migrator_helpers"
     for="Products.CMFPlone.interfaces.IPloneSiteRoot"
     class=".browser.ATCTMigratorHelpers"
@@ -69,6 +77,11 @@
       name="plone.app.contenttypes.migration.extendedtypes"
       provides="zope.schema.interfaces.IVocabularyFactory" />
 
+  <utility
+      factory=".vocabularies.ChangedBaseClasses"
+      name="plone.app.contenttypes.migration.changed_base_classes"
+      provides="zope.schema.interfaces.IVocabularyFactory" />
+
   <adapter name="nullmigrator" factory=".migration.BaseCustomMigator"/>
 
 </configure>
diff --git a/plone/app/contenttypes/migration/dxmigration.py b/plone/app/contenttypes/migration/dxmigration.py
index b5ce49a..09d1a0b 100644
--- a/plone/app/contenttypes/migration/dxmigration.py
+++ b/plone/app/contenttypes/migration/dxmigration.py
@@ -1,11 +1,16 @@
 # -*- coding: utf-8 -*-
+from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2Base
+from Products.CMFCore.utils import getToolByName
 from Products.contentmigration.basemigrator.migrator import CMFItemMigrator
 from Products.contentmigration.basemigrator.walker import CatalogWalker
 from plone.app.contenttypes.interfaces import IEvent
 from plone.app.contenttypes.migration import datetime_fixer
+from plone.dexterity.content import Container
+from plone.dexterity.content import Item
 from plone.event.utils import default_timezone
 from zope.annotation.interfaces import IAnnotations
 from zope.component.hooks import getSite
+import importlib
 
 
 def migrate(portal, migrator):
@@ -100,3 +105,80 @@ def migrate_schema_fields(self):
         old_text = annotations.get(
             'plone.app.event.dx.behaviors.IEventSummary.text', None)
         self.new.text = old_text
+
+
+def get_old_class_name_string(obj):
+    """Returns old class name string."""
+    return '{0}.{1}'.format(obj.__module__, obj.__class__.__name__)
+
+
+def get_portal_type_name_string(obj):
+    portal = getSite()
+    types = getToolByName(portal, "portal_types")
+    portal_type = types.get(obj.portal_type)
+    if not portal_type:
+        return
+
+    return portal_type.klass
+
+
+def migrate_base_class_to_new_class(obj,
+                                    indexes=[
+                                        'is_folderish',
+                                        'object_provides',
+                                    ],
+                                    ):
+    new_class_name = get_portal_type_name_string(obj)
+    current_class_name = get_old_class_name_string(obj)
+
+    if new_class_name == current_class_name:
+        return False
+
+    was_item = isinstance(obj, Item)
+    obj_id = obj.getId()
+    module_name, class_name = new_class_name.rsplit('.', 1)
+    module = importlib.import_module(module_name)
+    new_class = getattr(module, class_name)
+
+    # update obj class
+    parent = obj.__parent__
+    parent._delOb(obj_id)
+    obj.__class__ = new_class
+    parent._setOb(obj_id, obj)
+
+    is_container = isinstance(obj, Container)
+
+    # If object was Item likesh and becomes Containerish we have to do few
+    # preparation
+    if was_item and is_container:
+        # update obj _tree, because now it is container
+        BTreeFolder2Base._initBTrees(obj)
+
+    # reindex
+    obj.reindexObject(indexes)
+
+    return True
+
+
+def list_of_objects_with_changed_base_class():
+    portal = getSite()
+
+    catalog = getToolByName(portal, "portal_catalog")
+
+    for brain in catalog():
+        obj = brain.getObject()
+        if get_portal_type_name_string(obj) != get_old_class_name_string(obj):
+            yield obj
+
+
+def list_of_changed_base_class_names():
+    """Returns list of class names that are not longer in portal_types."""
+    changed_base_class_names = {}
+    for obj in list_of_objects_with_changed_base_class():
+        changed_base_class_name = get_old_class_name_string(obj)
+        if changed_base_class_name not in changed_base_class_names:
+            number_objects = changed_base_class_names.get(
+                changed_base_class_name, 0)
+            changed_base_class_names[changed_base_class_name] = \
+                number_objects + 1
+    return changed_base_class_names
diff --git a/plone/app/contenttypes/migration/vocabularies.py b/plone/app/contenttypes/migration/vocabularies.py
index 5c98154..12fcd9c 100644
--- a/plone/app/contenttypes/migration/vocabularies.py
+++ b/plone/app/contenttypes/migration/vocabularies.py
@@ -203,3 +203,19 @@ def __call__(self, context):
         extended fields.
         """
         return results(context, show_extended=True)
+
+
+class ChangedBaseClasses(object):
+    implements(IVocabularyFactory)
+
+    def __call__(self, context):
+        """Return a vocabulary with all changed base classes."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_changed_base_class_names
+        list_of_class_names = list_of_changed_base_class_names() or {}
+        return SimpleVocabulary(
+            [SimpleVocabulary.createTerm(
+                class_name, class_name,
+                '{0} ({1})'.format(class_name, list_of_class_names[class_name]))
+             for class_name in list_of_class_names.keys()]
+        )
diff --git a/plone/app/contenttypes/tests/test_migration.py b/plone/app/contenttypes/tests/test_migration.py
index fd6c0fc..011fc05 100644
--- a/plone/app/contenttypes/tests/test_migration.py
+++ b/plone/app/contenttypes/tests/test_migration.py
@@ -2,12 +2,19 @@
 from Products.CMFCore.utils import getToolByName
 from five.intid.intid import IntIds
 from five.intid.site import addUtility
+from lxml import etree
+from plone.app.contenttypes.testing import \
+    PLONE_APP_CONTENTTYPES_FUNCTIONAL_TESTING
 from plone.app.contenttypes.testing import \
     PLONE_APP_CONTENTTYPES_MIGRATION_TESTING
 from plone.app.contenttypes.testing import set_browserlayer
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
 from plone.app.testing import applyProfile
 from plone.app.testing import login
+from plone.dexterity.content import Container
 from plone.event.interfaces import IEventAccessor
+from plone.testing.z2 import Browser
 from zope.annotation.interfaces import IAnnotations
 from zope.component import getMultiAdapter
 from zope.component import getSiteManager
@@ -1374,3 +1381,125 @@ def get_portlets(context, columnName):
         dx_folder = self.portal['folder']
         self.failUnless('static-portlet' in get_portlets(dx_folder,
                                                          u'plone.rightcolumn'))
+
+
+class MigrateDexterityBaseClassIntegrationTest(unittest.TestCase):
+
+    layer = PLONE_APP_CONTENTTYPES_MIGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+
+        applyProfile(self.portal, 'plone.app.dexterity:testing')
+
+        self.portal.acl_users.userFolderAddUser('admin',
+                                                'secret',
+                                                ['Manager'],
+                                                [])
+        login(self.portal, 'admin')
+
+        # Add default content
+        self.portal.invokeFactory('Document', 'item')
+
+        # Change Document conent type to folderish
+        portal_types = getToolByName(self.portal, 'portal_types')
+        portal_types['Document'].klass = 'plone.dexterity.content.Container'
+        portal_types['Document'].allowed_content_types = ('Document',)
+
+    def test_dxmigration_migrate_item_to_container_class_is_changed(self):
+        """Check that base class was changed."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            migrate_base_class_to_new_class
+        migrate_base_class_to_new_class(self.portal.item)
+        self.assertTrue(isinstance(self.portal.item, Container))
+
+    def test_dxmigration_migrate_item_to_container_add_object_inside(self):
+        """Check that after migrate base class it can add items inside object.
+        """
+        from plone.app.contenttypes.migration.dxmigration import \
+            migrate_base_class_to_new_class
+        migrate_base_class_to_new_class(self.portal.item)
+        self.portal.item.invokeFactory('Document', 'doc')
+        self.assertEqual(len(self.portal.item.folderlistingFolderContents()), 1)
+
+    def test_dxmigration_migrate_list_of_objects_with_changed_base_class(self):
+        """Check list of objects with changed classes."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_objects_with_changed_base_class
+        # We have already one changed object
+        objects = [i for i in list_of_objects_with_changed_base_class()]
+        self.assertEqual(len(objects), 1)
+
+    def test_dxmigration_migrate_list_of_changed_base_class_names(self):
+        """Check list of changed base class names."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_changed_base_class_names
+        # We have already one changed object
+        names = [i for i in list_of_changed_base_class_names()]
+        self.assertEqual(len(names), 1)
+
+    def test_dxmigration_migrate_vocabulary_changed_base_classes(self):
+        """Check vocabulary of changed base class names."""
+        # We have already one changed object
+        name = 'plone.app.contenttypes.migration.changed_base_classes'
+        factory = getUtility(IVocabularyFactory, name)
+        vocabulary = factory(self.portal)
+        self.assertEqual(len(vocabulary), 1)
+
+
+class MigrateDexterityBaseClassFunctionalTest(unittest.TestCase):
+
+    layer = PLONE_APP_CONTENTTYPES_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+
+        self.portal_url = self.portal.absolute_url()
+        self.manage_document_url = '{0}/{1}/{2}/{3}'.format(
+            self.portal_url,
+            'portal_types',
+            'Document',
+            'manage_propertiesForm',
+        )
+
+        self.browser = Browser(app)
+        self.browser.handleErrors = False
+        self.browser.addHeader(
+            'Authorization',
+            'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
+        )
+
+        # Add default content
+        self.browser.open(self.portal_url)
+        self.browser.getLink('Page').click()
+        self.browser.getControl(name='form.widgets.IDublinCore.title')\
+            .value = "My item"
+        self.browser.getControl(name='form.widgets.IShortName.id')\
+            .value = "item"
+        self.browser.getControl('Save').click()
+
+        # Change Document conent type to folderish
+        self.browser.open(self.manage_document_url)
+        self.browser.getControl(name='klass:string') \
+            .value = 'plone.app.contenttypes.content.Collection'
+        self.browser.getControl('Save Changes').click()
+        self.browser.open(
+            '{0}/@@base_class_migrator_form'.format(self.portal_url))
+        self.good_info_message_template = 'There are {0} objects migrated.'
+
+    def test_dxmigration_migrate_check_migration_form_view(self):
+        """Check base class migrator view of changed base class names."""
+        html = etree.HTML(self.browser.contents)
+        checkboxes = html.xpath('//form//*[@name="{0}"]'.format(
+            'form.widgets.changed_base_classes:list'))
+        self.assertEqual(len(checkboxes), 1)
+
+    def test_dxmigration_migrate_check_migration_successful_message(self):
+        """Check base class migrator view of changed base class names."""
+        self.browser.getControl(name='form.widgets.changed_base_classes:list') \
+            .value = ['true']
+        self.browser.getControl('Update').click()
+        self.assertIn(
+            self.good_info_message_template.format(1), self.browser.contents)


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:34:39+01:00
Author: Bogdan Girman (bogdangi) <bogdan.girman@gmail.com>
Commit: https://github.com/plone/plone.app.contenttypes/commit/fcb3d1a2786746321758103b4e0c5f01f4dc53da

[#18] Add documentation to migrate all to folderish content type

Files changed:
M docs/README.rst

diff --git a/docs/README.rst b/docs/README.rst
index 240807e..a4fb850 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -218,6 +218,36 @@ Future versions of plone.app.contenttypes will have with a form that allows you
 However if you'd like to migrate your content-types to Dexterity before this feature is completed you might want to have a look at the code of plone.app.contenttypes.migration.migration.NewsItemMigrator as a blueprint for a migration.
 
 
+Migrating non folderish to folderish objects (or change base class for any dexterety base type)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Migration non folderish object to folderish has few steps.
+
+Step 1 (Update base content types folderish)
+-------------------------------------------
+
+Apply `folderish` profile.
+
+- Go to portal_setup->Import;
+
+- Select `Plone default content-types (Folderish behavior) - dexterity`
+
+- Click  `Import all steps`
+
+
+Step 2 (Update base class for exist objects)
+--------------------------------------------
+
+There is update changed base class migration step which allow you to update base class if last one was changed.
+
+To get this migration form you have to open follow link `PORTAL_URL/@@base_class_migrator_form`.
+
+On this view you will see check boxes with class name string and numbers of changed objects that are going to be updated.
+Select classes that you want to update and click on button `Update`.
+
+If migration was successful you see the info box with number of successfully updated objects or warning with number of not updated objects.
+
+
 Widgets
 -------
 


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:34:39+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/7043ce25de4359f257a5ae229f924e6f2f65dbc5

allow folderish collections

Files changed:
A plone/app/contenttypes/profiles/folderish/types/Collection.xml
M plone/app/contenttypes/content.py
M plone/app/contenttypes/profiles/folderish/types.xml

diff --git a/plone/app/contenttypes/content.py b/plone/app/contenttypes/content.py
index dcd673f..07b366d 100644
--- a/plone/app/contenttypes/content.py
+++ b/plone/app/contenttypes/content.py
@@ -14,6 +14,8 @@
 
 @implementer(ICollection)
 class Collection(Item):
+    """Convinience Item subclass for ``Collection`` portal type
+    """
     # BBB
 
     def listMetaDataFields(self, exclude=True):
@@ -69,47 +71,54 @@ class File(Item):
 
 @implementer(IFolder)
 class Folder(Container):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Folder`` portal type
     """
 
 
 @implementer(IImage)
 class Image(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Image`` portal type
     """
 
 
 @implementer(ILink)
 class Link(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Link`` portal type
     """
 
 
 @implementer(INewsItem)
 class NewsItem(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``News Item`` portal type
     """
 
 
 @implementer(IEvent)
 class Event(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Event`` portal type
+    """
+
+# Folderish subclasses used by 'profile-plone.app.contenttypes:folderish'
+
+@implementer(ICollection)
+class FolderishCollection(Container):
+    """Folderish convinience subclass for ``Collection`` portal type
     """
 
 
 @implementer(IDocument)
 class FolderishDocument(Container):
-    """Convinience subclass for ``File`` portal type
+    """Folderish convinience subclass for ``Document`` portal type
     """
 
 
 @implementer(IEvent)
 class FolderishEvent(Container):
-    """Convinience subclass for ``File`` portal type
+    """Folderish convinience subclass for ``Event`` portal type
     """
 
 
 @implementer(INewsItem)
 class FolderishNewsItem(Container):
-    """Convinience subclass for ``File`` portal type
+    """Folderish convinience subclass for ``News Item`` portal type
     """
diff --git a/plone/app/contenttypes/profiles/folderish/types.xml b/plone/app/contenttypes/profiles/folderish/types.xml
index 34e8b69..740c1e8 100644
--- a/plone/app/contenttypes/profiles/folderish/types.xml
+++ b/plone/app/contenttypes/profiles/folderish/types.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0"?>
 <object meta_type="Plone Types Tool" name="portal_types">
+ <object meta_type="Dexterity FTI" name="Collection" />
  <object meta_type="Dexterity FTI" name="Document" />
  <object meta_type="Dexterity FTI" name="News Item" />
  <object meta_type="Dexterity FTI" name="Event" />
diff --git a/plone/app/contenttypes/profiles/folderish/types/Collection.xml b/plone/app/contenttypes/profiles/folderish/types/Collection.xml
new file mode 100644
index 0000000..a396a48
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Collection.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Collection" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishCollection</property>
+</object>


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:34:39+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/076cef22402645bc476b81ed77e2f0eb55677b3b

update documentation

Files changed:
M docs/README.rst

diff --git a/docs/README.rst b/docs/README.rst
index a4fb850..b2ea4ab 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -218,35 +218,27 @@ Future versions of plone.app.contenttypes will have with a form that allows you
 However if you'd like to migrate your content-types to Dexterity before this feature is completed you might want to have a look at the code of plone.app.contenttypes.migration.migration.NewsItemMigrator as a blueprint for a migration.
 
 
-Migrating non folderish to folderish objects (or change base class for any dexterety base type)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Migrating to folderish objects or change base class for any dexterity type
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 Migration non folderish object to folderish has few steps.
 
-Step 1 (Update base content types folderish)
--------------------------------------------
-
-Apply `folderish` profile.
-
-- Go to portal_setup->Import;
+Step 1: Enable folderish types
+++++++++++++++++++++++++++++++
 
-- Select `Plone default content-types (Folderish behavior) - dexterity`
-
-- Click  `Import all steps`
+See `Using folderish types`_
 
 
-Step 2 (Update base class for exist objects)
---------------------------------------------
+Step 2: Update base class for existing objects
+++++++++++++++++++++++++++++++++++++++++++++++
 
-There is update changed base class migration step which allow you to update base class if last one was changed.
+If you changed the base-class of existing types you might also want to upgrade the base-class of existing objects. You can use the following form for this: `PORTAL_URL/@@base_class_migrator_form`.
 
-To get this migration form you have to open follow link `PORTAL_URL/@@base_class_migrator_form`.
+On this you will see checkboxes with class names and the amount of objects that are going to be updated. Select classes that you want to update and click on button `Update`.
 
-On this view you will see check boxes with class name string and numbers of changed objects that are going to be updated.
-Select classes that you want to update and click on button `Update`.
-
-If migration was successful you see the info box with number of successfully updated objects or warning with number of not updated objects.
+If the migration was successful you see the info box with number of successfully updated objects or a warning with number of not updated objects.
 
+This form can be used to change the base-class of any dexterity-types instances.
 
 Widgets
 -------
@@ -323,6 +315,29 @@ If you want to add plone.app.contenttypes as a dependency from another products
 If you use the profile ``default`` then the default-content in new sites will still be Archetypes-based. You'll then have to migrate that content using the migration-form ``@@atct_migrator`` or delete it by hand.
 
 
+Using folderish types
+^^^^^^^^^^^^^^^^^^^^^
+
+If you want all types folderish you need to depend on the profile ``folderish`` in your own ``metadata.xml``.
+
+.. code:: xml
+
+    <metadata>
+      <version>1</version>
+        <dependencies>
+            <dependency>profile-plone.app.contenttypes:folderish</dependency>
+        </dependencies>
+    </metadata>
+
+You can also enable this profile by hand by applying the `folderish` profile:
+
+- Go to portal_setup->Import;
+- Select `Plone default content-types (Folderish behavior) - dexterity`
+- Click  `Import all steps`
+
+You can also migrate your existing objects to folderish types. For details see `Step 2: Update base class for existing objects`_
+
+
 Extending the types
 ^^^^^^^^^^^^^^^^^^^
 


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T11:39:57+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/5c126a25bf10fa27d7ae4c28836a9a6ab17db5af

Merge branch 'folderish-optional' of git://github.com/plone/plone.app.contenttypes into folderish-optional

Conflicts:
	plone/app/contenttypes/tests/test_migration.py

Files changed:
M plone/app/contenttypes/tests/test_migration.py

diff --git a/plone/app/contenttypes/tests/test_migration.py b/plone/app/contenttypes/tests/test_migration.py
index 011fc05..de60823 100644
--- a/plone/app/contenttypes/tests/test_migration.py
+++ b/plone/app/contenttypes/tests/test_migration.py
@@ -1289,6 +1289,7 @@ def test_migration_view_confirmation(self):
         results = migration_view()
         self.assertIn('@@migrate_from_atct?migrate=1', results)
 
+
     def test_portlets_are_migrated(self):
         """add portlets and see if they're still available on the migrated
         content including portlet settings.


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T12:22:08+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/7d80fe0156ea456808cef76bbc60fb6fbef69692

check of multilingual in form to change base-class

Files changed:
M plone/app/contenttypes/migration/dxmigration.py
M plone/app/contenttypes/migration/vocabularies.py

diff --git a/plone/app/contenttypes/migration/dxmigration.py b/plone/app/contenttypes/migration/dxmigration.py
index 09d1a0b..dcd32cb 100644
--- a/plone/app/contenttypes/migration/dxmigration.py
+++ b/plone/app/contenttypes/migration/dxmigration.py
@@ -5,12 +5,19 @@
 from Products.contentmigration.basemigrator.walker import CatalogWalker
 from plone.app.contenttypes.interfaces import IEvent
 from plone.app.contenttypes.migration import datetime_fixer
+from plone.app.contenttypes.migration.utils import HAS_MULTILINGUAL
 from plone.dexterity.content import Container
 from plone.dexterity.content import Item
+from plone.dexterity.interfaces import IDexterityContent
 from plone.event.utils import default_timezone
+from zExceptions import NotFound
 from zope.annotation.interfaces import IAnnotations
 from zope.component.hooks import getSite
+
 import importlib
+import logging
+
+logger = logging.getLogger(__name__)
 
 
 def migrate(portal, migrator):
@@ -148,10 +155,8 @@ def migrate_base_class_to_new_class(obj,
 
     is_container = isinstance(obj, Container)
 
-    # If object was Item likesh and becomes Containerish we have to do few
-    # preparation
     if was_item and is_container:
-        # update obj _tree, because now it is container
+        #  If Itemish becomes Folderish we have to update obj _tree
         BTreeFolder2Base._initBTrees(obj)
 
     # reindex
@@ -160,21 +165,25 @@ def migrate_base_class_to_new_class(obj,
     return True
 
 
-def list_of_objects_with_changed_base_class():
-    portal = getSite()
-
-    catalog = getToolByName(portal, "portal_catalog")
-
-    for brain in catalog():
-        obj = brain.getObject()
+def list_of_objects_with_changed_base_class(context):
+    catalog = getToolByName(context, "portal_catalog")
+    query = {'object_provides': IDexterityContent.__identifier__}
+    if HAS_MULTILINGUAL and 'Language' in catalog.indexes():
+        query['Language'] = 'all'
+    for brain in catalog(query):
+        try:
+            obj = brain.getObject()
+        except NotFound:
+            logger.warn("Object {0} not found".format(brain.getPath()))
+            continue
         if get_portal_type_name_string(obj) != get_old_class_name_string(obj):
             yield obj
 
 
-def list_of_changed_base_class_names():
+def list_of_changed_base_class_names(context):
     """Returns list of class names that are not longer in portal_types."""
     changed_base_class_names = {}
-    for obj in list_of_objects_with_changed_base_class():
+    for obj in list_of_objects_with_changed_base_class(context):
         changed_base_class_name = get_old_class_name_string(obj)
         if changed_base_class_name not in changed_base_class_names:
             number_objects = changed_base_class_names.get(
diff --git a/plone/app/contenttypes/migration/vocabularies.py b/plone/app/contenttypes/migration/vocabularies.py
index 12fcd9c..905812b 100644
--- a/plone/app/contenttypes/migration/vocabularies.py
+++ b/plone/app/contenttypes/migration/vocabularies.py
@@ -212,7 +212,7 @@ def __call__(self, context):
         """Return a vocabulary with all changed base classes."""
         from plone.app.contenttypes.migration.dxmigration import \
             list_of_changed_base_class_names
-        list_of_class_names = list_of_changed_base_class_names() or {}
+        list_of_class_names = list_of_changed_base_class_names(context) or {}
         return SimpleVocabulary(
             [SimpleVocabulary.createTerm(
                 class_name, class_name,


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T12:38:51+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/56b6016bbe88da16d8fd70ff952536454e259c81

fix displaying the amount of items to be migrated

Files changed:
M plone/app/contenttypes/migration/dxmigration.py

diff --git a/plone/app/contenttypes/migration/dxmigration.py b/plone/app/contenttypes/migration/dxmigration.py
index dcd32cb..f96b0c4 100644
--- a/plone/app/contenttypes/migration/dxmigration.py
+++ b/plone/app/contenttypes/migration/dxmigration.py
@@ -186,8 +186,7 @@ def list_of_changed_base_class_names(context):
     for obj in list_of_objects_with_changed_base_class(context):
         changed_base_class_name = get_old_class_name_string(obj)
         if changed_base_class_name not in changed_base_class_names:
-            number_objects = changed_base_class_names.get(
-                changed_base_class_name, 0)
-            changed_base_class_names[changed_base_class_name] = \
-                number_objects + 1
+            changed_base_class_names[changed_base_class_name] = 1
+        else:
+            changed_base_class_names[changed_base_class_name] += 1
     return changed_base_class_names


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-07T12:48:01+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/99e465ce295c638b74ba6cdddf2364a845dbfe03

fix tests

Files changed:
M plone/app/contenttypes/tests/test_migration.py

diff --git a/plone/app/contenttypes/tests/test_migration.py b/plone/app/contenttypes/tests/test_migration.py
index de60823..18ac6f8 100644
--- a/plone/app/contenttypes/tests/test_migration.py
+++ b/plone/app/contenttypes/tests/test_migration.py
@@ -1428,7 +1428,7 @@ def test_dxmigration_migrate_list_of_objects_with_changed_base_class(self):
         from plone.app.contenttypes.migration.dxmigration import \
             list_of_objects_with_changed_base_class
         # We have already one changed object
-        objects = [i for i in list_of_objects_with_changed_base_class()]
+        objects = [i for i in list_of_objects_with_changed_base_class(self.portal)]
         self.assertEqual(len(objects), 1)
 
     def test_dxmigration_migrate_list_of_changed_base_class_names(self):
@@ -1436,7 +1436,7 @@ def test_dxmigration_migrate_list_of_changed_base_class_names(self):
         from plone.app.contenttypes.migration.dxmigration import \
             list_of_changed_base_class_names
         # We have already one changed object
-        names = [i for i in list_of_changed_base_class_names()]
+        names = [i for i in list_of_changed_base_class_names(self.portal)]
         self.assertEqual(len(names), 1)
 
     def test_dxmigration_migrate_vocabulary_changed_base_classes(self):


Repository: plone.app.contenttypes
Branch: refs/heads/master
Date: 2014-11-13T17:03:15+01:00
Author: Philip Bauer (pbauer) <bauer@starzel.de>
Commit: https://github.com/plone/plone.app.contenttypes/commit/8c4a7f2bf7a175efd655915b5d8930a36883f761

Merge pull request #187 from plone/folderish-optional

Folderish optional

Files changed:
A plone/app/contenttypes/profiles/folderish/metadata.xml
A plone/app/contenttypes/profiles/folderish/types.xml
A plone/app/contenttypes/profiles/folderish/types/Collection.xml
A plone/app/contenttypes/profiles/folderish/types/Document.xml
A plone/app/contenttypes/profiles/folderish/types/Event.xml
A plone/app/contenttypes/profiles/folderish/types/News_Item.xml
M docs/README.rst
M plone/app/contenttypes/content.py
M plone/app/contenttypes/migration/browser.py
M plone/app/contenttypes/migration/configure.zcml
M plone/app/contenttypes/migration/dxmigration.py
M plone/app/contenttypes/migration/vocabularies.py
M plone/app/contenttypes/profiles.zcml
M plone/app/contenttypes/tests/test_migration.py

diff --git a/docs/README.rst b/docs/README.rst
index 240807e..b2ea4ab 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -218,6 +218,28 @@ Future versions of plone.app.contenttypes will have with a form that allows you
 However if you'd like to migrate your content-types to Dexterity before this feature is completed you might want to have a look at the code of plone.app.contenttypes.migration.migration.NewsItemMigrator as a blueprint for a migration.
 
 
+Migrating to folderish objects or change base class for any dexterity type
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Migration non folderish object to folderish has few steps.
+
+Step 1: Enable folderish types
+++++++++++++++++++++++++++++++
+
+See `Using folderish types`_
+
+
+Step 2: Update base class for existing objects
+++++++++++++++++++++++++++++++++++++++++++++++
+
+If you changed the base-class of existing types you might also want to upgrade the base-class of existing objects. You can use the following form for this: `PORTAL_URL/@@base_class_migrator_form`.
+
+On this you will see checkboxes with class names and the amount of objects that are going to be updated. Select classes that you want to update and click on button `Update`.
+
+If the migration was successful you see the info box with number of successfully updated objects or a warning with number of not updated objects.
+
+This form can be used to change the base-class of any dexterity-types instances.
+
 Widgets
 -------
 
@@ -293,6 +315,29 @@ If you want to add plone.app.contenttypes as a dependency from another products
 If you use the profile ``default`` then the default-content in new sites will still be Archetypes-based. You'll then have to migrate that content using the migration-form ``@@atct_migrator`` or delete it by hand.
 
 
+Using folderish types
+^^^^^^^^^^^^^^^^^^^^^
+
+If you want all types folderish you need to depend on the profile ``folderish`` in your own ``metadata.xml``.
+
+.. code:: xml
+
+    <metadata>
+      <version>1</version>
+        <dependencies>
+            <dependency>profile-plone.app.contenttypes:folderish</dependency>
+        </dependencies>
+    </metadata>
+
+You can also enable this profile by hand by applying the `folderish` profile:
+
+- Go to portal_setup->Import;
+- Select `Plone default content-types (Folderish behavior) - dexterity`
+- Click  `Import all steps`
+
+You can also migrate your existing objects to folderish types. For details see `Step 2: Update base class for existing objects`_
+
+
 Extending the types
 ^^^^^^^^^^^^^^^^^^^
 
diff --git a/plone/app/contenttypes/content.py b/plone/app/contenttypes/content.py
index 5b8d4e5..07b366d 100644
--- a/plone/app/contenttypes/content.py
+++ b/plone/app/contenttypes/content.py
@@ -14,6 +14,8 @@
 
 @implementer(ICollection)
 class Collection(Item):
+    """Convinience Item subclass for ``Collection`` portal type
+    """
     # BBB
 
     def listMetaDataFields(self, exclude=True):
@@ -69,29 +71,54 @@ class File(Item):
 
 @implementer(IFolder)
 class Folder(Container):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Folder`` portal type
     """
 
 
 @implementer(IImage)
 class Image(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Image`` portal type
     """
 
 
 @implementer(ILink)
 class Link(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Link`` portal type
     """
 
 
 @implementer(INewsItem)
 class NewsItem(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``News Item`` portal type
     """
 
 
 @implementer(IEvent)
 class Event(Item):
-    """Convinience subclass for ``File`` portal type
+    """Convinience subclass for ``Event`` portal type
+    """
+
+# Folderish subclasses used by 'profile-plone.app.contenttypes:folderish'
+
+@implementer(ICollection)
+class FolderishCollection(Container):
+    """Folderish convinience subclass for ``Collection`` portal type
+    """
+
+
+@implementer(IDocument)
+class FolderishDocument(Container):
+    """Folderish convinience subclass for ``Document`` portal type
+    """
+
+
+@implementer(IEvent)
+class FolderishEvent(Container):
+    """Folderish convinience subclass for ``Event`` portal type
+    """
+
+
+@implementer(INewsItem)
+class FolderishNewsItem(Container):
+    """Folderish convinience subclass for ``News Item`` portal type
     """
diff --git a/plone/app/contenttypes/migration/browser.py b/plone/app/contenttypes/migration/browser.py
index 841b6d3..112481f 100644
--- a/plone/app/contenttypes/migration/browser.py
+++ b/plone/app/contenttypes/migration/browser.py
@@ -10,6 +10,7 @@
 from datetime import datetime
 from datetime import timedelta
 from plone.app.contenttypes.migration import migration
+from plone.app.contenttypes.migration import dxmigration
 from plone.app.contenttypes.migration.utils import HAS_MULTILINGUAL
 from plone.app.contenttypes.migration.utils import installTypeIfNeeded
 from plone.app.contenttypes.migration.utils import isSchemaExtended
@@ -362,6 +363,65 @@ def updateWidgets(self):
 )
 
 
+class IBaseClassMigratorForm(Interface):
+
+    changed_base_classes = schema.List(
+        title=u'Changed base classes',
+        description=u'Select changed base classes you want to migrate',
+        value_type=schema.Choice(
+            vocabulary='plone.app.contenttypes.migration.changed_base_classes',
+        ),
+        required=True,
+    )
+
+
+class BaseClassMigratorForm(form.Form):
+
+    fields = field.Fields(IBaseClassMigratorForm)
+    fields['changed_base_classes'].widgetFactory = CheckBoxFieldWidget
+    ignoreContext = True
+    enableCSRFProtection = True
+
+    @button.buttonAndHandler(u'Update', name='update')
+    def handle_migrate(self, action):
+        data, errors = self.extractData()
+
+        if errors:
+            return
+
+        changed_base_classes = data.get('changed_base_classes', [])
+        if not changed_base_classes:
+            return
+
+        catalog = getToolByName(self.context, "portal_catalog")
+        migrated = []
+        not_migrated = []
+        for brain in catalog():
+            obj = brain.getObject()
+            old_class_name = dxmigration.get_old_class_name_string(obj)
+            if old_class_name in changed_base_classes:
+                if dxmigration.migrate_base_class_to_new_class(obj):
+                    migrated.append(obj)
+                else:
+                    not_migrated.append(obj)
+
+        messages = IStatusMessage(self.request)
+        info_message_template = 'There are {0} objects migrated.'
+        warn_message_template = 'There are not {0} objects migrated.'
+        if migrated:
+            msg = info_message_template.format(len(migrated))
+            messages.addStatusMessage(msg, type='info')
+        if not_migrated:
+            msg = warn_message_template.format(len(not_migrated))
+            messages.addStatusMessage(msg, type='warn')
+        self.request.response.redirect(self.request['ACTUAL_URL'])
+
+
+BaseClassMigrator = wrap_form(
+    BaseClassMigratorForm,
+)
+
+
 class ATCTMigratorHelpers(BrowserView):
 
     def objects_to_be_migrated(self):
diff --git a/plone/app/contenttypes/migration/configure.zcml b/plone/app/contenttypes/migration/configure.zcml
index e912337..9881860 100644
--- a/plone/app/contenttypes/migration/configure.zcml
+++ b/plone/app/contenttypes/migration/configure.zcml
@@ -36,6 +36,14 @@
     />
 
   <browser:page
+    name="base_class_migrator_form"
+    for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+    class=".browser.BaseClassMigrator"
+    layer="plone.app.contenttypes.interfaces.IPloneAppContenttypesLayer"
+    permission="cmf.ManagePortal"
+    />
+
+  <browser:page
     name="atct_migrator_helpers"
     for="Products.CMFPlone.interfaces.IPloneSiteRoot"
     class=".browser.ATCTMigratorHelpers"
@@ -69,6 +77,11 @@
       name="plone.app.contenttypes.migration.extendedtypes"
       provides="zope.schema.interfaces.IVocabularyFactory" />
 
+  <utility
+      factory=".vocabularies.ChangedBaseClasses"
+      name="plone.app.contenttypes.migration.changed_base_classes"
+      provides="zope.schema.interfaces.IVocabularyFactory" />
+
   <adapter name="nullmigrator" factory=".migration.BaseCustomMigator"/>
 
 </configure>
diff --git a/plone/app/contenttypes/migration/dxmigration.py b/plone/app/contenttypes/migration/dxmigration.py
index b5ce49a..f96b0c4 100644
--- a/plone/app/contenttypes/migration/dxmigration.py
+++ b/plone/app/contenttypes/migration/dxmigration.py
@@ -1,12 +1,24 @@
 # -*- coding: utf-8 -*-
+from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2Base
+from Products.CMFCore.utils import getToolByName
 from Products.contentmigration.basemigrator.migrator import CMFItemMigrator
 from Products.contentmigration.basemigrator.walker import CatalogWalker
 from plone.app.contenttypes.interfaces import IEvent
 from plone.app.contenttypes.migration import datetime_fixer
+from plone.app.contenttypes.migration.utils import HAS_MULTILINGUAL
+from plone.dexterity.content import Container
+from plone.dexterity.content import Item
+from plone.dexterity.interfaces import IDexterityContent
 from plone.event.utils import default_timezone
+from zExceptions import NotFound
 from zope.annotation.interfaces import IAnnotations
 from zope.component.hooks import getSite
 
+import importlib
+import logging
+
+logger = logging.getLogger(__name__)
+
 
 def migrate(portal, migrator):
     """return a CatalogWalker instance in order
@@ -100,3 +112,81 @@ def migrate_schema_fields(self):
         old_text = annotations.get(
             'plone.app.event.dx.behaviors.IEventSummary.text', None)
         self.new.text = old_text
+
+
+def get_old_class_name_string(obj):
+    """Returns old class name string."""
+    return '{0}.{1}'.format(obj.__module__, obj.__class__.__name__)
+
+
+def get_portal_type_name_string(obj):
+    portal = getSite()
+    types = getToolByName(portal, "portal_types")
+    portal_type = types.get(obj.portal_type)
+    if not portal_type:
+        return
+
+    return portal_type.klass
+
+
+def migrate_base_class_to_new_class(obj,
+                                    indexes=[
+                                        'is_folderish',
+                                        'object_provides',
+                                    ],
+                                    ):
+    new_class_name = get_portal_type_name_string(obj)
+    current_class_name = get_old_class_name_string(obj)
+
+    if new_class_name == current_class_name:
+        return False
+
+    was_item = isinstance(obj, Item)
+    obj_id = obj.getId()
+    module_name, class_name = new_class_name.rsplit('.', 1)
+    module = importlib.import_module(module_name)
+    new_class = getattr(module, class_name)
+
+    # update obj class
+    parent = obj.__parent__
+    parent._delOb(obj_id)
+    obj.__class__ = new_class
+    parent._setOb(obj_id, obj)
+
+    is_container = isinstance(obj, Container)
+
+    if was_item and is_container:
+        #  If Itemish becomes Folderish we have to update obj _tree
+        BTreeFolder2Base._initBTrees(obj)
+
+    # reindex
+    obj.reindexObject(indexes)
+
+    return True
+
+
+def list_of_objects_with_changed_base_class(context):
+    catalog = getToolByName(context, "portal_catalog")
+    query = {'object_provides': IDexterityContent.__identifier__}
+    if HAS_MULTILINGUAL and 'Language' in catalog.indexes():
+        query['Language'] = 'all'
+    for brain in catalog(query):
+        try:
+            obj = brain.getObject()
+        except NotFound:
+            logger.warn("Object {0} not found".format(brain.getPath()))
+            continue
+        if get_portal_type_name_string(obj) != get_old_class_name_string(obj):
+            yield obj
+
+
+def list_of_changed_base_class_names(context):
+    """Returns list of class names that are not longer in portal_types."""
+    changed_base_class_names = {}
+    for obj in list_of_objects_with_changed_base_class(context):
+        changed_base_class_name = get_old_class_name_string(obj)
+        if changed_base_class_name not in changed_base_class_names:
+            changed_base_class_names[changed_base_class_name] = 1
+        else:
+            changed_base_class_names[changed_base_class_name] += 1
+    return changed_base_class_names
diff --git a/plone/app/contenttypes/migration/vocabularies.py b/plone/app/contenttypes/migration/vocabularies.py
index 5c98154..905812b 100644
--- a/plone/app/contenttypes/migration/vocabularies.py
+++ b/plone/app/contenttypes/migration/vocabularies.py
@@ -203,3 +203,19 @@ def __call__(self, context):
         extended fields.
         """
         return results(context, show_extended=True)
+
+
+class ChangedBaseClasses(object):
+    implements(IVocabularyFactory)
+
+    def __call__(self, context):
+        """Return a vocabulary with all changed base classes."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_changed_base_class_names
+        list_of_class_names = list_of_changed_base_class_names(context) or {}
+        return SimpleVocabulary(
+            [SimpleVocabulary.createTerm(
+                class_name, class_name,
+                '{0} ({1})'.format(class_name, list_of_class_names[class_name]))
+             for class_name in list_of_class_names.keys()]
+        )
diff --git a/plone/app/contenttypes/profiles.zcml b/plone/app/contenttypes/profiles.zcml
index 886bd84..b2a315e 100644
--- a/plone/app/contenttypes/profiles.zcml
+++ b/plone/app/contenttypes/profiles.zcml
@@ -20,6 +20,16 @@
     provides="Products.GenericSetup.interfaces.EXTENSION"
     />
 
+  <!-- All types, News Item, Document and Event folderish, no sample content,
+       depends on default -->
+  <gs:registerProfile
+    name="folderish"
+    title="Plone default content-types (Folderish behavior) - dexterity"
+    directory="profiles/folderish"
+    description="This provides Plones default types without adding example comntent"
+    provides="Products.GenericSetup.interfaces.EXTENSION"
+    />
+
   <!-- Uninstall -->
   <gs:registerProfile
     name="uninstall"
diff --git a/plone/app/contenttypes/profiles/folderish/metadata.xml b/plone/app/contenttypes/profiles/folderish/metadata.xml
new file mode 100644
index 0000000..743d12c
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/metadata.xml
@@ -0,0 +1,6 @@
+<metadata>
+ <version>1</version>
+ <dependencies>
+  <dependency>profile-plone.app.contenttypes:default</dependency>
+ </dependencies>
+</metadata>
diff --git a/plone/app/contenttypes/profiles/folderish/types.xml b/plone/app/contenttypes/profiles/folderish/types.xml
new file mode 100644
index 0000000..740c1e8
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<object meta_type="Plone Types Tool" name="portal_types">
+ <object meta_type="Dexterity FTI" name="Collection" />
+ <object meta_type="Dexterity FTI" name="Document" />
+ <object meta_type="Dexterity FTI" name="News Item" />
+ <object meta_type="Dexterity FTI" name="Event" />
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Collection.xml b/plone/app/contenttypes/profiles/folderish/types/Collection.xml
new file mode 100644
index 0000000..a396a48
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Collection.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Collection" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishCollection</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Document.xml b/plone/app/contenttypes/profiles/folderish/types/Document.xml
new file mode 100644
index 0000000..6c997f2
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Document.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Document" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishDocument</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/Event.xml b/plone/app/contenttypes/profiles/folderish/types/Event.xml
new file mode 100644
index 0000000..710f9a9
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/Event.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="Event" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishEvent</property>
+</object>
diff --git a/plone/app/contenttypes/profiles/folderish/types/News_Item.xml b/plone/app/contenttypes/profiles/folderish/types/News_Item.xml
new file mode 100644
index 0000000..c1a8a30
--- /dev/null
+++ b/plone/app/contenttypes/profiles/folderish/types/News_Item.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<object name="News Item" meta_type="Dexterity FTI" i18n:domain="plone"
+   xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+ <property name="filter_content_types">False</property>
+ <property name="klass">plone.app.contenttypes.content.FolderishNewsItem</property> 
+</object>
diff --git a/plone/app/contenttypes/tests/test_migration.py b/plone/app/contenttypes/tests/test_migration.py
index fd6c0fc..18ac6f8 100644
--- a/plone/app/contenttypes/tests/test_migration.py
+++ b/plone/app/contenttypes/tests/test_migration.py
@@ -2,12 +2,19 @@
 from Products.CMFCore.utils import getToolByName
 from five.intid.intid import IntIds
 from five.intid.site import addUtility
+from lxml import etree
+from plone.app.contenttypes.testing import \
+    PLONE_APP_CONTENTTYPES_FUNCTIONAL_TESTING
 from plone.app.contenttypes.testing import \
     PLONE_APP_CONTENTTYPES_MIGRATION_TESTING
 from plone.app.contenttypes.testing import set_browserlayer
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
 from plone.app.testing import applyProfile
 from plone.app.testing import login
+from plone.dexterity.content import Container
 from plone.event.interfaces import IEventAccessor
+from plone.testing.z2 import Browser
 from zope.annotation.interfaces import IAnnotations
 from zope.component import getMultiAdapter
 from zope.component import getSiteManager
@@ -1282,6 +1289,7 @@ def test_migration_view_confirmation(self):
         results = migration_view()
         self.assertIn('@@migrate_from_atct?migrate=1', results)
 
+
     def test_portlets_are_migrated(self):
         """add portlets and see if they're still available on the migrated
         content including portlet settings.
@@ -1374,3 +1382,125 @@ def get_portlets(context, columnName):
         dx_folder = self.portal['folder']
         self.failUnless('static-portlet' in get_portlets(dx_folder,
                                                          u'plone.rightcolumn'))
+
+
+class MigrateDexterityBaseClassIntegrationTest(unittest.TestCase):
+
+    layer = PLONE_APP_CONTENTTYPES_MIGRATION_TESTING
+
+    def setUp(self):
+        self.portal = self.layer['portal']
+
+        applyProfile(self.portal, 'plone.app.dexterity:testing')
+
+        self.portal.acl_users.userFolderAddUser('admin',
+                                                'secret',
+                                                ['Manager'],
+                                                [])
+        login(self.portal, 'admin')
+
+        # Add default content
+        self.portal.invokeFactory('Document', 'item')
+
+        # Change Document conent type to folderish
+        portal_types = getToolByName(self.portal, 'portal_types')
+        portal_types['Document'].klass = 'plone.dexterity.content.Container'
+        portal_types['Document'].allowed_content_types = ('Document',)
+
+    def test_dxmigration_migrate_item_to_container_class_is_changed(self):
+        """Check that base class was changed."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            migrate_base_class_to_new_class
+        migrate_base_class_to_new_class(self.portal.item)
+        self.assertTrue(isinstance(self.portal.item, Container))
+
+    def test_dxmigration_migrate_item_to_container_add_object_inside(self):
+        """Check that after migrate base class it can add items inside object.
+        """
+        from plone.app.contenttypes.migration.dxmigration import \
+            migrate_base_class_to_new_class
+        migrate_base_class_to_new_class(self.portal.item)
+        self.portal.item.invokeFactory('Document', 'doc')
+        self.assertEqual(len(self.portal.item.folderlistingFolderContents()), 1)
+
+    def test_dxmigration_migrate_list_of_objects_with_changed_base_class(self):
+        """Check list of objects with changed classes."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_objects_with_changed_base_class
+        # We have already one changed object
+        objects = [i for i in list_of_objects_with_changed_base_class(self.portal)]
+        self.assertEqual(len(objects), 1)
+
+    def test_dxmigration_migrate_list_of_changed_base_class_names(self):
+        """Check list of changed base class names."""
+        from plone.app.contenttypes.migration.dxmigration import \
+            list_of_changed_base_class_names
+        # We have already one changed object
+        names = [i for i in list_of_changed_base_class_names(self.portal)]
+        self.assertEqual(len(names), 1)
+
+    def test_dxmigration_migrate_vocabulary_changed_base_classes(self):
+        """Check vocabulary of changed base class names."""
+        # We have already one changed object
+        name = 'plone.app.contenttypes.migration.changed_base_classes'
+        factory = getUtility(IVocabularyFactory, name)
+        vocabulary = factory(self.portal)
+        self.assertEqual(len(vocabulary), 1)
+
+
+class MigrateDexterityBaseClassFunctionalTest(unittest.TestCase):
+
+    layer = PLONE_APP_CONTENTTYPES_FUNCTIONAL_TESTING
+
+    def setUp(self):
+        app = self.layer['app']
+        self.portal = self.layer['portal']
+        self.request = self.layer['request']
+
+        self.portal_url = self.portal.absolute_url()
+        self.manage_document_url = '{0}/{1}/{2}/{3}'.format(
+            self.portal_url,
+            'portal_types',
+            'Document',
+            'manage_propertiesForm',
+        )
+
+        self.browser = Browser(app)
+        self.browser.handleErrors = False
+        self.browser.addHeader(
+            'Authorization',
+            'Basic %s:%s' % (SITE_OWNER_NAME, SITE_OWNER_PASSWORD,)
+        )
+
+        # Add default content
+        self.browser.open(self.portal_url)
+        self.browser.getLink('Page').click()
+        self.browser.getControl(name='form.widgets.IDublinCore.title')\
+            .value = "My item"
+        self.browser.getControl(name='form.widgets.IShortName.id')\
+            .value = "item"
+        self.browser.getControl('Save').click()
+
+        # Change Document conent type to folderish
+        self.browser.open(self.manage_document_url)
+        self.browser.getControl(name='klass:string') \
+            .value = 'plone.app.contenttypes.content.Collection'
+        self.browser.getControl('Save Changes').click()
+        self.browser.open(
+            '{0}/@@base_class_migrator_form'.format(self.portal_url))
+        self.good_info_message_template = 'There are {0} objects migrated.'
+
+    def test_dxmigration_migrate_check_migration_form_view(self):
+        """Check base class migrator view of changed base class names."""
+        html = etree.HTML(self.browser.contents)
+        checkboxes = html.xpath('//form//*[@name="{0}"]'.format(
+            'form.widgets.changed_base_classes:list'))
+        self.assertEqual(len(checkboxes), 1)
+
+    def test_dxmigration_migrate_check_migration_successful_message(self):
+        """Check base class migrator view of changed base class names."""
+        self.browser.getControl(name='form.widgets.changed_base_classes:list') \
+            .value = ['true']
+        self.browser.getControl('Update').click()
+        self.assertIn(
+            self.good_info_message_template.format(1), self.browser.contents)