Repository: plone/Products.CMFPlone

Issue:
TTW Bundle compilation broken
Since the integration of plone.staticresources / Plone 5.2 the TTW bundle compilation in ``@@resourceregistry-controlpanel`` is broken.
r.js cannot be found.
Dexterity Control Panel is missing if site is created without default/example content
Plone 5.0.2



Discussion:

still the case in coredev 5.1

I can confirm this issue; I think the problem is plone.app.contenttypes is not being installed and Dexterity was installed by it.


Resolving patch:
diff --git a/Products/CMFPlone/Portal.py b/Products/CMFPlone/Portal.py
--- a/Products/CMFPlone/Portal.py
+++ b/Products/CMFPlone/Portal.py
@@ -1,20 +1,30 @@
 from AccessControl import ClassSecurityInfo
-from AccessControl import Permissions
 from AccessControl import Unauthorized
 from AccessControl.class_init import InitializeClass
 from Acquisition import aq_base
 from ComputedAttribute import ComputedAttribute
+from five.localsitemanager.registry import PersistentComponents
 from OFS.ObjectManager import REPLACEABLE
+from plone.dexterity.content import Container
+from plone.i18n.locales.interfaces import IMetadataLanguageAvailability
 from Products.CMFCore import permissions
+from Products.CMFCore.interfaces import IContentish
+from Products.CMFCore.interfaces import ISiteRoot
+from Products.CMFCore.permissions import AccessContentsInformation
+from Products.CMFCore.permissions import AddPortalMember
+from Products.CMFCore.permissions import MailForgottenPassword
+from Products.CMFCore.permissions import RequestReview
+from Products.CMFCore.permissions import ReviewPortalContent
+from Products.CMFCore.permissions import SetOwnPassword
+from Products.CMFCore.permissions import SetOwnProperties
+from Products.CMFCore.PortalFolder import PortalFolderBase
 from Products.CMFCore.PortalObject import PortalObjectBase
-from Products.CMFCore.utils import UniqueObject
+from Products.CMFCore.Skinnable import SkinnableObjectManager
 from Products.CMFCore.utils import _checkPermission
 from Products.CMFCore.utils import getToolByName
-from Products.CMFDynamicViewFTI.browserdefault import BrowserDefaultMixin
-from Products.CMFPlone import PloneMessageFactory as _
+from Products.CMFCore.utils import UniqueObject
 from Products.CMFPlone import bbb
-from Products.CMFPlone.DublinCore import DefaultDublinCoreImpl
-from Products.CMFPlone.PloneFolder import OrderedContainer
+from Products.CMFPlone import PloneMessageFactory as _
 from Products.CMFPlone.interfaces.siteroot import IPloneSiteRoot
 from Products.CMFPlone.interfaces.syndication import ISyndicatable
 from Products.CMFPlone.permissions import AddPortalContent
@@ -23,27 +33,67 @@
 from Products.CMFPlone.permissions import ModifyPortalContent
 from Products.CMFPlone.permissions import ReplyToItem
 from Products.CMFPlone.permissions import View
-from plone.i18n.locales.interfaces import IMetadataLanguageAvailability
+from Products.Five.component.interfaces import IObjectManagerSite
 from zope.component import queryUtility
+from zope.component.interfaces import ComponentLookupError
+from zope.event import notify
+from zope.interface import classImplementsOnly
+from zope.interface import implementedBy
 from zope.interface import implementer
+from zope.traversing.interfaces import BeforeTraverseEvent
+
 
 if bbb.HAS_ZSERVER:
     from webdav.NullResource import NullResource
 
 
-@implementer(IPloneSiteRoot, ISyndicatable)
-class PloneSite(PortalObjectBase, DefaultDublinCoreImpl, OrderedContainer,
-                BrowserDefaultMixin, UniqueObject):
+@implementer(IPloneSiteRoot, ISiteRoot, ISyndicatable, IObjectManagerSite)
+class PloneSite(Container, SkinnableObjectManager, UniqueObject):
     """ The Plone site object. """
 
     security = ClassSecurityInfo()
     meta_type = portal_type = 'Plone Site'
 
+    # Ensure certain attributes come from the correct base class.
+    _checkId = SkinnableObjectManager._checkId
+    manage_main = PortalFolderBase.manage_main
+
+    def __getattr__(self, name):
+        try:
+            # Try DX
+            return super().__getattr__(name)
+        except AttributeError:
+            # Check portal_skins
+            return SkinnableObjectManager.__getattr__(self, name)
+
+    def __setattr__(self, name, obj):
+        # handle re setting an item as an attribute
+        if self._tree is not None and name in self:
+            del self[name]
+            self[name] = obj
+        else:
+            super().__setattr__(name, obj)
+
+    def __delattr__(self, name):
+        try:
+            return super().__delattr__(name)
+        except AttributeError:
+            return self.__delitem__(name)
+
+    # Removes the 'Components Folder'
+
     manage_options = (
-        PortalObjectBase.manage_options[:2] +
-        PortalObjectBase.manage_options[3:])
+        Container.manage_options[:2] +
+        Container.manage_options[3:])
 
     __ac_permissions__ = (
+        (AccessContentsInformation, ()),
+        (AddPortalMember, ()),
+        (SetOwnPassword, ()),
+        (SetOwnProperties, ()),
+        (MailForgottenPassword, ()),
+        (RequestReview, ()),
+        (ReviewPortalContent, ()),
         (AddPortalContent, ()),
         (AddPortalFolders, ()),
         (ListPortalMembers, ()),
@@ -53,13 +103,6 @@ class PloneSite(PortalObjectBase, DefaultDublinCoreImpl, OrderedContainer,
                                'manage_renameForm', 'manage_renameObject',
                                'manage_renameObjects')))
 
-    security.declareProtected(Permissions.copy_or_move, 'manage_copyObjects')
-
-    manage_renameObject = OrderedContainer.manage_renameObject
-
-    moveObject = OrderedContainer.moveObject
-    moveObjectsByDelta = OrderedContainer.moveObjectsByDelta
-
     # Switch off ZMI ordering interface as it assumes a slightly
     # different functionality
     has_order_support = 0
@@ -73,9 +116,28 @@ class PloneSite(PortalObjectBase, DefaultDublinCoreImpl, OrderedContainer,
     description = ''
     icon = 'misc_/CMFPlone/tool.gif'
 
+    # From PortalObjectBase
     def __init__(self, id, title=''):
-        PortalObjectBase.__init__(self, id, title)
-        DefaultDublinCoreImpl.__init__(self)
+        super(PloneSite, self).__init__(id, title=title)
+        components = PersistentComponents('++etc++site')
+        components.__parent__ = self
+        self.setSiteManager(components)
+
+    # From PortalObjectBase
+    def __before_publishing_traverse__(self, arg1, arg2=None):
+        """ Pre-traversal hook.
+        """
+        # XXX hack around a bug(?) in BeforeTraverse.MultiHook
+        REQUEST = arg2 or arg1
+
+        try:
+            notify(BeforeTraverseEvent(self, REQUEST))
+        except ComponentLookupError:
+            # allow ZMI access, even if the portal's site manager is missing
+            pass
+        self.setupCurrentSkin(REQUEST)
+
+        super(PloneSite, self).__before_publishing_traverse__(arg1, arg2)
 
     def __browser_default__(self, request):
         """ Set default so we can return whatever we want instead
@@ -173,4 +235,9 @@ def reindexObject(self, idxs=None):
     def reindexObjectSecurity(self, skip_self=False):
         pass
 
+
+# Remove the IContentish interface so we don't listen to events that won't
+# apply to the site root, ie handleUidAnnotationEvent
+classImplementsOnly(PloneSite, implementedBy(PloneSite) - IContentish)
+
 InitializeClass(PloneSite)
diff --git a/Products/CMFPlone/browser/admin.py b/Products/CMFPlone/browser/admin.py
--- a/Products/CMFPlone/browser/admin.py
+++ b/Products/CMFPlone/browser/admin.py
@@ -70,7 +70,12 @@ def sites(self, root=None):
         return result
 
     def outdated(self, obj):
-        mig = obj.get('portal_migration', None)
+        # Try to pick the portal_migration as an attribute
+        # (Plone 5 unmigrated site root) or as an item
+        mig = (
+            getattr(obj, "portal_migration", None)
+            or obj.get('portal_migration', None)
+        )
         if mig is not None:
             return mig.needUpgrading()
         return False
diff --git a/Products/CMFPlone/factory.py b/Products/CMFPlone/factory.py
--- a/Products/CMFPlone/factory.py
+++ b/Products/CMFPlone/factory.py
@@ -1,5 +1,7 @@
 from logging import getLogger
 from plone.registry.interfaces import IRegistry
+from plone.uuid.handlers import addAttributeUUID
+from Products.CMFCore.interfaces import ISiteRoot
 from Products.CMFPlone import PloneMessageFactory as _
 from Products.CMFPlone.events import SiteManagerCreatedEvent
 from Products.CMFPlone.interfaces import INonInstallable
@@ -10,6 +12,7 @@
 from zope.component.hooks import setSite
 from zope.event import notify
 from zope.interface import implementer
+from zope.lifecycleevent import ObjectCreatedEvent
 
 _TOOL_ID = 'portal_setup'
 _DEFAULT_PROFILE = 'Products.CMFPlone:plone'
@@ -120,8 +123,12 @@ def addPloneSite(context, site_id, title='Plone site', description='',
                  extension_ids=(), setup_content=True,
                  default_language='en', portal_timezone='UTC'):
     """Add a PloneSite to the context."""
-    context._setObject(site_id, PloneSite(site_id))
-    site = context._getOb(site_id)
+
+    site = PloneSite(site_id)
+    notify(ObjectCreatedEvent(site))
+    context[site_id] = site
+
+    site = context[site_id]
     site.setLanguage(default_language)
     # Set the accepted language for the rest of the request.  This makes sure
     # the front-page text gets the correct translation also when your browser


Test patch:
diff --git a/Products/CMFPlone/tests/testActionsTool.py b/Products/CMFPlone/tests/testActionsTool.py
--- a/Products/CMFPlone/tests/testActionsTool.py
+++ b/Products/CMFPlone/tests/testActionsTool.py
@@ -64,7 +64,7 @@ def testMissingActionProvider(self):
             self.fail_tb('Should not bomb out if a provider is missing')
 
     def testBrokenActionProvider(self):
-        self.portal.portal_types = None
+        self.portal.portal_types = self.portal.portal_catalog
         try:
             self.actions.listFilteredActionsFor(self.portal)
         except:
diff --git a/Products/CMFPlone/tests/testCatalogTool.py b/Products/CMFPlone/tests/testCatalogTool.py
--- a/Products/CMFPlone/tests/testCatalogTool.py
+++ b/Products/CMFPlone/tests/testCatalogTool.py
@@ -31,7 +31,7 @@
 group2 = 'g2'
 
 base_content = ['Members', 'aggregator', 'aggregator',
-                'events', 'news', TEST_USER_ID, 'front-page', 'doc']
+                'events', 'news', TEST_USER_ID, 'doc']
 
 
 class TestCatalogSetup(PloneTestCase):
diff --git a/Products/CMFPlone/tests/testPloneFolder.py b/Products/CMFPlone/tests/testPloneFolder.py
--- a/Products/CMFPlone/tests/testPloneFolder.py
+++ b/Products/CMFPlone/tests/testPloneFolder.py
@@ -3,7 +3,7 @@
 
 from Products.CMFPlone.utils import _createObjectByType
 
-from AccessControl import Unauthorized
+from zExceptions import Unauthorized
 from Products.CMFCore.permissions import DeleteObjects
 
 from zExceptions import BadRequest
diff --git a/Products/CMFPlone/tests/testPortalCreation.py b/Products/CMFPlone/tests/testPortalCreation.py
--- a/Products/CMFPlone/tests/testPortalCreation.py
+++ b/Products/CMFPlone/tests/testPortalCreation.py
@@ -419,18 +419,16 @@ def testTTWLockableProperty(self):
         self.assertEqual(True, registry['plone.lock_on_ttw_edit'])
 
     def testPortalFTIIsDynamicFTI(self):
-        # Plone Site FTI should be a DynamicView FTI
+        # Plone Site FTI should be a Dexterity FTI
         fti = self.portal.getTypeInfo()
-        self.assertEqual(
-            fti.meta_type, 'Factory-based Type Information with dynamic views'
-        )
+        self.assertEqual(fti.meta_type, 'Dexterity FTI')
 
     def testPloneSiteFTIHasMethodAliases(self):
         # Should add method aliases to the Plone Site FTI
         expected_aliases = {
             '(Default)': '(dynamic view)',
             'view': '(selected layout)',
-            'edit': '@@site-controlpanel',
+            'edit': '@@edit',
             'sharing': '@@sharing',
         }
         fti = self.portal.getTypeInfo()
@@ -532,13 +530,6 @@ def testSyndicationTabDisabled(self):
                     "Actions tool still has visible 'syndication' action"
                 )
 
-    def testObjectButtonActionsInvisibleOnPortalRoot(self):
-        # only a manager would have proper permissions
-        self.setRoles(['Manager', 'Member'])
-        acts = self.actions.listFilteredActionsFor(self.portal)
-        buttons = acts.get('object_buttons', [])
-        self.assertEqual(0, len(buttons))
-
     def testObjectButtonActionsInvisibleOnPortalDefaultDocument(self):
         # only a manager would have proper permissions
         self.setRoles(['Manager', 'Member'])
@@ -1023,8 +1014,11 @@ def test_addsite_en_as_nl(self):
         self.request.form['default_language'] = 'en'
         self.addsite()
         plonesite = self.app.plonesite1
-        fp = plonesite['front-page']
         # Unfortunately, the next test passes even without the fix (overriding
         # HTTP_ACCEPT_LANGUAGE on the request in factory.py).  This seems to be
         # because translations are not available in the tests.
-        self.assertIn('Learn more about Plone', fp.text.raw)
+        self.assertIn('Learn more about Plone', plonesite.text.raw)
+
+        # XXX maybe it is better to reset the sire in the @@plone-addsite view
+        # or somewhere else?
+        setSite(None)
diff --git a/Products/CMFPlone/tests/testWebDAV.py b/Products/CMFPlone/tests/testWebDAV.py
--- a/Products/CMFPlone/tests/testWebDAV.py
+++ b/Products/CMFPlone/tests/testWebDAV.py
@@ -303,8 +303,6 @@ def testPUTIndexHtml(self):
     def testPUTIndexHtmlIntoPortal(self):
         # Create an index_html document in the portal via FTP/DAV
         self.assertFalse('index_html' in self.portal)
-        self.assertEqual(self.portal.index_html.meta_type,
-                         'Filesystem Page Template')
         self.setRoles(['Manager'])
 
         response = self.publish(
diff --git a/Products/CMFPlone/tests/test_safe_formatter.py b/Products/CMFPlone/tests/test_safe_formatter.py
--- a/Products/CMFPlone/tests/test_safe_formatter.py
+++ b/Products/CMFPlone/tests/test_safe_formatter.py
@@ -118,7 +118,7 @@ def test_cook_zope2_page_templates_good_format_attr_str(self):
         hack_pt(pt, self.portal)
         self.assertEqual(
             pt.pt_render().strip(),
-            '<p>title of &lt;PloneSite at plone&gt; is Plone site</p>')
+            '<p>title of &lt;PloneSite at plone&gt; is Welcome to Plone</p>')
 
     def test_cook_zope2_page_templates_good_format_attr_unicode(self):
         from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate
@@ -126,7 +126,7 @@ def test_cook_zope2_page_templates_good_format_attr_unicode(self):
         hack_pt(pt, self.portal)
         self.assertEqual(
             pt.pt_render().strip(),
-            '<p>title of &lt;PloneSite at plone&gt; is Plone site</p>')
+            '<p>title of &lt;PloneSite at plone&gt; is Welcome to Plone</p>')
 
     def test_access_to_private_content_not_allowed_via_rich_text(self):
         try: