diff --git a/Products/CMFPlone/browser/admin.py b/Products/CMFPlone/browser/admin.py index 0e1df304b4..1c91c8e97d 100644 --- a/Products/CMFPlone/browser/admin.py +++ b/Products/CMFPlone/browser/admin.py @@ -6,7 +6,6 @@ from importlib.metadata import distribution from importlib.metadata import PackageNotFoundError from OFS.interfaces import IApplication -from plone.base.interfaces import INonInstallable from plone.base.interfaces import IPloneSiteRoot from plone.base.utils import get_installer from plone.i18n.locales.interfaces import IContentLanguageAvailability @@ -15,16 +14,13 @@ from plone.protect.interfaces import IDisableCSRFProtection from Products.CMFCore.permissions import ManagePortal from Products.CMFPlone.factory import _DEFAULT_PROFILE +from Products.CMFPlone.factory import _TYPES_PROFILE from Products.CMFPlone.factory import addPloneSite from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile -from Products.GenericSetup import BASE -from Products.GenericSetup import EXTENSION -from Products.GenericSetup import profile_registry from Products.GenericSetup.upgrade import normalize_version from urllib import parse from ZODB.broken import Broken from zope.component import adapter -from zope.component import getAllUtilitiesRegisteredFor from zope.component import getUtility from zope.component import queryMultiAdapter from zope.component import queryUtility @@ -46,6 +42,11 @@ HAS_VOLTO = True except PackageNotFoundError: HAS_VOLTO = False +try: + distribution("plone.app.caching") + HAS_CACHING = True +except PackageNotFoundError: + HAS_CACHING = False try: distribution("plone.app.upgrade") HAS_UPGRADE = True @@ -67,8 +68,6 @@ def publishTraverse(self, request, name): class Overview(BrowserView): - has_volto = HAS_VOLTO - def sites(self, root=None): if root is None: root = self.context @@ -155,67 +154,15 @@ class FrontPage(BrowserView): class AddPloneSite(BrowserView): - # Profiles that are installed by default, - # but can be removed later. - default_extension_profiles = ( - "plone.app.caching:default", - "plonetheme.barceloneta:default", - ) - # Let's have a separate list for Volto. - volto_default_extension_profiles = ( - "plone.app.caching:default", - "plonetheme.barceloneta:default", - "plone.volto:default", - ) - - def profiles(self): - base_profiles = [] - extension_profiles = [] - if HAS_VOLTO and not self.request.get("classic"): - selected_extension_profiles = self.volto_default_extension_profiles - else: - selected_extension_profiles = self.default_extension_profiles - - # profiles available for install/uninstall, but hidden at the time - # the Plone site is created - not_installable = [ - "Products.CMFPlacefulWorkflow:CMFPlacefulWorkflow", - ] - utils = getAllUtilitiesRegisteredFor(INonInstallable) - for util in utils: - not_installable.extend( - util.getNonInstallableProfiles() - if hasattr(util, "getNonInstallableProfiles") - else [] - ) - - for info in profile_registry.listProfileInfo(): - if info.get("type") == EXTENSION and info.get("for") in ( - IPloneSiteRoot, - None, - ): - profile_id = info.get("id") - if profile_id not in not_installable: - if profile_id in selected_extension_profiles: - info["selected"] = "selected" - extension_profiles.append(info) - - def _key(v): - # Make sure implicitly selected items come first - selected = v.get("selected") and "automatic" or "manual" - return "{}-{}".format(selected, v.get("title", "")) - - extension_profiles.sort(key=_key) - - for info in profile_registry.listProfileInfo(): - if info.get("type") == BASE and info.get("for") in (IPloneSiteRoot, None): - base_profiles.append(info) - - return dict( - base=tuple(base_profiles), - default=_DEFAULT_PROFILE, - extensions=tuple(extension_profiles), - ) + @property + def default_extension_profiles(self): + # Profiles that are installed by default, + # but can be removed later. + profiles = [_TYPES_PROFILE] + if HAS_CACHING: + profiles.append("plone.app.caching:default") + profiles.append("plonetheme.barceloneta:default") + return profiles def browser_language(self): language = "en" @@ -308,9 +255,8 @@ def __call__(self): context, site_id, title=form.get("title", ""), - profile_id=form.get("profile_id", _DEFAULT_PROFILE), - extension_ids=form.get("extension_ids", ()), - setup_content=form.get("setup_content", False), + profile_id=_DEFAULT_PROFILE, + extension_ids=self.default_extension_profiles, default_language=form.get("default_language", "en"), portal_timezone=form.get("portal_timezone", "UTC"), ) diff --git a/Products/CMFPlone/browser/admin.zcml b/Products/CMFPlone/browser/admin.zcml index 8d6696c408..04af682344 100644 --- a/Products/CMFPlone/browser/admin.zcml +++ b/Products/CMFPlone/browser/admin.zcml @@ -1,6 +1,7 @@ - + + - + + + + + + + - - - -
+ tal:attributes="action string:${context/absolute_url}/@@plone-addsite">

Create a Plone site

@@ -120,92 +115,6 @@
-
-
- - -
- Should the default example content be added to the site? -
-
-
- - - -
-
-

Base configuration

- -
- - -
${info/description}
-
- -
- You normally don't need to change anything here unless you have specific reasons and know what you are doing. -
- -
-
- - -
- -

Add-ons

- -
- Select any add-ons you want to activate immediately. - You can also activate add-ons after the site has been created using the Add-ons control panel. -
-
- - - - -
- - -
- ${info/description} -
-
-
- - - -
-
-
diff --git a/Products/CMFPlone/browser/templates/plone-overview.pt b/Products/CMFPlone/browser/templates/plone-overview.pt index 035f0b4856..42217e8180 100644 --- a/Products/CMFPlone/browser/templates/plone-overview.pt +++ b/Products/CMFPlone/browser/templates/plone-overview.pt @@ -87,24 +87,9 @@ - Create Classic UI Plone site - Advanced + i18n:translate="">Create Classic UI Plone site
-

- Starting with Plone 6, 'Create a new Plone site' applies a - profile and creates default content for the new React based - default frontend Volto. You are however required to set up and run - an additional frontend service to use this setup. -

The 'Create Classic UI Plone site' button creates a Plone site configured for HTML based output, as was already supported by previous Plone versions. @@ -117,6 +102,11 @@ these frontends and possible upgrade paths from older Plone versions to Plone 6.

+

+ Starting with Plone 6.1, default content is not loaded into the site. + If you want to load the default content, you should install the plone.classicui package. + If you see this text, that means you have not installed that package yet. +

diff --git a/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_search.py b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_search.py index f0b6c31daa..8598ada339 100644 --- a/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_search.py +++ b/Products/CMFPlone/controlpanel/tests/test_controlpanel_browser_search.py @@ -29,7 +29,9 @@ def setUp(self): def test_search_control_panel_link(self): self.browser.open("%s/@@overview-controlpanel" % self.portal_url) - self.browser.getLink("Search").click() + # The first "Search" link actually points to "Advanced Search"... + # self.browser.getLink("Search").click() + self.browser.getLink(url=f"{self.portal_url}/@@search-controlpanel").click() def test_search_control_panel_backlink(self): self.browser.open("%s/@@search-controlpanel" % self.portal_url) diff --git a/Products/CMFPlone/factory.py b/Products/CMFPlone/factory.py index 5506b7bb02..4b652b0e5c 100644 --- a/Products/CMFPlone/factory.py +++ b/Products/CMFPlone/factory.py @@ -10,6 +10,8 @@ from zope.interface import implementer from zope.lifecycleevent import ObjectCreatedEvent +import warnings + _TOOL_ID = "portal_setup" _DEFAULT_PROFILE = "Products.CMFPlone:plone" @@ -130,14 +132,53 @@ def addPloneSite( title="Plone site", description="", profile_id=_DEFAULT_PROFILE, - content_profile_id=None, snapshot=False, + content_profile_id=None, extension_ids=(), - setup_content=True, + setup_content=None, default_language="en", portal_timezone="UTC", + distribution_name=None, + **kwargs, ): """Add a PloneSite to the context.""" + if distribution_name: + from plone.distribution.api import site as site_api + + # Pass all arguments and keyword arguments in the answers, + # But the 'distribution_name' is not needed there. + answers = { + "site_id": site_id, + "title": title, + "description": description, + "profile_id": profile_id, + "snapshot": snapshot, + "content_profile_id": content_profile_id, + "extension_ids": extension_ids, + "setup_content": setup_content, + "default_language": default_language, + "portal_timezone": portal_timezone, + } + answers.update(kwargs) + site = site_api._create_site( + context=context, distribution_name=distribution_name, answers=answers + ) + setSite(site) + return site + + if content_profile_id is not None: + warnings.warn( + "addPloneSite ignores the content_profile_id keyword argument " + "since Plone 6.1. In Plone 7 it will be removed.", + DeprecationWarning, + ) + if setup_content is not None: + warnings.warn( + "addPloneSite ignores the setup_content keyword argument " + "since Plone 6.1, treating it as always False. " + "In Plone 7 it will be removed.", + DeprecationWarning, + ) site = PloneSite(site_id) notify(ObjectCreatedEvent(site)) @@ -169,24 +210,6 @@ def addPloneSite( reg["plone.available_languages"] = [default_language] reg["plone.site_title"] = title - # Install default content types profile if user do not select "example content" - # during site creation. - if setup_content: - if content_profile_id: - content_profiles = [content_profile_id] - elif "plone.volto:default" in extension_ids: - content_profiles = [ - _TYPES_PROFILE, - "plone.volto:default-homepage", - ] - else: - content_profiles = [_CONTENT_PROFILE] - else: - content_profiles = [_TYPES_PROFILE] - - for profile_id in content_profiles: - setup_tool.runAllImportStepsFromProfile(f"profile-{profile_id}") - props = dict( title=title, description=description, diff --git a/Products/CMFPlone/patches/__init__.py b/Products/CMFPlone/patches/__init__.py index e8234dc395..a931d15b38 100644 --- a/Products/CMFPlone/patches/__init__.py +++ b/Products/CMFPlone/patches/__init__.py @@ -1,7 +1,3 @@ -# FIXME: This no longer works with the new ZMI -# from . import addzmiplonesite # Add an explicit link to add a new Plone -# site to the ZMI for faster access - from . import addzmisecuritywarning # Add a warning to the ZMI security tab from . import csrf # Protects most important methods from from . import dateIndexPatch # Avoid OverflowErrors in Date*Indexes diff --git a/Products/CMFPlone/patches/addzmiplonesite.py b/Products/CMFPlone/patches/addzmiplonesite.py deleted file mode 100644 index a772ab71cd..0000000000 --- a/Products/CMFPlone/patches/addzmiplonesite.py +++ /dev/null @@ -1,42 +0,0 @@ -from OFS.ObjectManager import ObjectManager - - -# FIXME: This no longer works with the new ZMI - -ADD_PLONE_SITE_HTML = """ - - -
- - - -
-
- - - -
- The site configuration is outdated and needs to be upgraded. - - Please continue with the upgrade. - -
-
-
-""" - -main = ObjectManager.manage_main -orig = main.read() -pos = orig.find("") - -# Add in our button html at the right position -new = orig[:pos] + ADD_PLONE_SITE_HTML + orig[pos:] - -# Modify the manage_main -main.edited_source = new -main._v_cooked = main.cook() diff --git a/Products/CMFPlone/testing.py b/Products/CMFPlone/testing.py index 4b178f00b0..63f65ce3b9 100644 --- a/Products/CMFPlone/testing.py +++ b/Products/CMFPlone/testing.py @@ -89,4 +89,29 @@ def tearDownPloneSite(self, portal): name="CMFPloneLayer:Acceptance", ) + +class ProductsCMFPloneDistributionsLayer(ProductsCMFPloneLayer): + defaultBases = (PRODUCTS_CMFPLONE_FIXTURE,) + + def setUpZope(self, app, configurationContext): + import Products.CMFPlone + + xmlconfig.file( + "configure-distributions.zcml", + Products.CMFPlone.tests, + context=configurationContext, + ) + + def setUpPloneSite(self, portal): + # ProductsCMFPloneLayer already does enough setup. + pass + + +PRODUCTS_CMFPLONE_DISTRIBUTIONS_FIXTURE = ProductsCMFPloneDistributionsLayer() + +PRODUCTS_CMFPLONE_DISTRIBUTIONS_INTEGRATION_TESTING = IntegrationTesting( + bases=(PRODUCTS_CMFPLONE_DISTRIBUTIONS_FIXTURE,), + name="CMFPloneLayer:DistributionsIntegration", +) + optionflags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE diff --git a/Products/CMFPlone/tests/configure-distributions.zcml b/Products/CMFPlone/tests/configure-distributions.zcml new file mode 100644 index 0000000000..eb1428852e --- /dev/null +++ b/Products/CMFPlone/tests/configure-distributions.zcml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/Products/CMFPlone/tests/configure.zcml b/Products/CMFPlone/tests/configure.zcml index bd6ca7c2db..29f6057348 100644 --- a/Products/CMFPlone/tests/configure.zcml +++ b/Products/CMFPlone/tests/configure.zcml @@ -1,6 +1,8 @@ diff --git a/Products/CMFPlone/tests/testPortalCreation.py b/Products/CMFPlone/tests/testPortalCreation.py index 57d7959e3c..200c18217c 100644 --- a/Products/CMFPlone/tests/testPortalCreation.py +++ b/Products/CMFPlone/tests/testPortalCreation.py @@ -1,4 +1,6 @@ from Acquisition import aq_base +from importlib.metadata import distribution +from importlib.metadata import PackageNotFoundError from plone.base.interfaces import IFilterSchema from plone.base.interfaces import INavigationSchema from plone.base.interfaces import ISearchSchema @@ -34,6 +36,15 @@ from zope.interface.interfaces import IComponentRegistry from zope.location.interfaces import ISite +import unittest + + +try: + distribution("plone.distribution") + HAS_DISTRIBUTION = True +except PackageNotFoundError: + HAS_DISTRIBUTION = False + class TestPortalCreation(PloneTestCase.PloneTestCase): def afterSetUp(self): @@ -920,6 +931,10 @@ def testManagementPageCharset(self): self.assertEqual(manage_charset, "utf-8") +@unittest.skipIf( + HAS_DISTRIBUTION, + "@@plone-addsite is not available because plone.distribution is used.", +) class TestAddPloneSite(PloneTestCase.PloneTestCase): def afterSetUp(self): self.request = self.app.REQUEST @@ -930,7 +945,6 @@ def addsite(self): form = self.request.form form["form.submitted"] = 1 form["site_id"] = "plonesite1" - form["setup_content"] = 1 self.request["_authenticator"] = createToken() addsite = self.app.restrictedTraverse("@@plone-addsite") addsite() @@ -946,6 +960,6 @@ def test_addsite_en_as_nl(self): # because translations are not available in the tests. self.assertIn("Learn more about Plone", plonesite.text.raw) - # XXX maybe it is better to reset the sire in the @@plone-addsite view + # XXX maybe it is better to reset the site in the @@plone-addsite view # or somewhere else? setSite(None) diff --git a/Products/CMFPlone/tests/test_factory.py b/Products/CMFPlone/tests/test_factory.py index b876f730eb..435c2f5a1d 100644 --- a/Products/CMFPlone/tests/test_factory.py +++ b/Products/CMFPlone/tests/test_factory.py @@ -1,7 +1,11 @@ +from importlib.metadata import distribution from plone.dexterity.interfaces import IDexterityFTI from plone.registry.interfaces import IRegistry from Products.CMFPlone.factory import addPloneSite from Products.CMFPlone.testing import PRODUCTS_CMFPLONE_INTEGRATION_TESTING +from Products.CMFPlone.testing import ( + PRODUCTS_CMFPLONE_DISTRIBUTIONS_INTEGRATION_TESTING, +) from Products.CMFPlone.utils import get_installer from zope.component import getUtility from zope.component import queryUtility @@ -9,6 +13,13 @@ import unittest +try: + distribution("plone.distribution") + HAS_DISTRIBUTION = True +except PackageNotFoundError: + HAS_DISTRIBUTION = False + + class TestFactoryPloneSite(unittest.TestCase): layer = PRODUCTS_CMFPLONE_INTEGRATION_TESTING @@ -18,7 +29,7 @@ def setUp(self): def testPlonesiteWithUnicodeTitle(self): TITLE = "Ploné" - ploneSite = addPloneSite(self.app, "ploneFoo", title=TITLE, setup_content=False) + ploneSite = addPloneSite(self.app, "ploneFoo", title=TITLE) ploneSiteTitleProperty = ploneSite.getProperty("title") # CMF stores title as string only so Plone should keep the same track self.assertTrue(isinstance(ploneSiteTitleProperty, str)) @@ -29,7 +40,7 @@ def testPlonesiteWithUnicodeTitle(self): def testPlonesiteWithoutUnicodeTitle(self): TITLE = "Plone" - ploneSite = addPloneSite(self.app, "ploneFoo", title=TITLE, setup_content=False) + ploneSite = addPloneSite(self.app, "ploneFoo", title=TITLE) ploneSiteTitleProperty = ploneSite.getProperty("title") # CMF stores title as string only so Plone should keep the same track self.assertTrue(isinstance(ploneSiteTitleProperty, str)) @@ -38,21 +49,57 @@ def testPlonesiteWithoutUnicodeTitle(self): self.assertTrue(isinstance(ploneSiteTitle, str)) self.assertEqual(ploneSiteTitle, TITLE) - def test_site_creation_without_content_but_with_dexterity(self): - """Test site creation without example content have dexterity installed.""" - ploneSite = addPloneSite(self.app, "ploneFoo", title="Foo", setup_content=False) + def test_site_creation_has_no_dexterity(self): + """Test site creation does not even have dexterity installed. + + If you want it, you need to pass more extension_ids, + like the plone-addsite view does. + """ + ploneSite = addPloneSite(self.app, "ploneFoo", title="Foo") qi = get_installer(ploneSite, self.request) - self.assertTrue(qi.is_product_installed("plone.app.dexterity")) + self.assertFalse(qi.is_product_installed("plone.app.dexterity")) - def test_site_creation_without_content_but_with_content_types(self): - """Test site creation without example content have content types.""" - addPloneSite(self.app, "ploneFoo", title="Foo", setup_content=False) + def test_site_creation_has_no_content_types(self): + """Test site creation has no content types. + + If you want them, you need to pass more extension_ids, + like the plone-addsite view does. + """ + addPloneSite(self.app, "ploneFoo", title="Foo") # Folder fti = queryUtility(IDexterityFTI, name="Folder") + self.assertIsNone(fti) + # For good measure we check that there is at least on FTI. + fti = queryUtility(IDexterityFTI, name="Plone Site") self.assertIsNotNone(fti) def test_site_creation_title_is_set_in_registry(self): """Plone site title should be stored in registry""" - ploneSite = addPloneSite(self.app, "ploneFoo", title="Foo", setup_content=False) + ploneSite = addPloneSite(self.app, "ploneFoo", title="Foo") registry = getUtility(IRegistry, context=ploneSite) self.assertEqual(registry["plone.site_title"], "Foo") + + +class TestFactoryDistributionPloneSite(unittest.TestCase): + layer = PRODUCTS_CMFPLONE_DISTRIBUTIONS_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.request = self.layer["request"] + + @unittest.skipIf( + not HAS_DISTRIBUTION, + "Passing a distribution_name needs plone.distribution.", + ) + def test_site_creation_distribution(self): + """Create a Plone Site using a distribution""" + ploneSite = addPloneSite( + self.app, + "ploneFoo", + title="Foo", + distribution_name="testdistro", + default_language="nl", + ) + self.assertEqual(ploneSite.getId(), "ploneFoo") + self.assertEqual(ploneSite.title, "Foo") + self.assertEqual(ploneSite.Language(), "nl") diff --git a/news/3961.breaking.1 b/news/3961.breaking.1 new file mode 100644 index 0000000000..0bd48e1547 --- /dev/null +++ b/news/3961.breaking.1 @@ -0,0 +1,2 @@ +Only register the add site form and root Zope overview if `plone.distribution` is not available. +[maurits] diff --git a/news/3961.breaking.2 b/news/3961.breaking.2 new file mode 100644 index 0000000000..6b21bc0d27 --- /dev/null +++ b/news/3961.breaking.2 @@ -0,0 +1,5 @@ +Remove advanced options from Add Plone Site form. +If you need more options, you should add a Plone Distribution to your packages. +The main ones are `plone.volto` and `plone.classicui`. +We now only create a basic Plone site without default example content. +[maurits] diff --git a/news/3961.breaking.3 b/news/3961.breaking.3 new file mode 100644 index 0000000000..38030bb3b4 --- /dev/null +++ b/news/3961.breaking.3 @@ -0,0 +1,4 @@ +`factory.addPloneSite`: remove `setup_content` and `content_profile_id` keyword arguments. +We no longer load default content. Use a Plone Distribution if you need this. +Or pass an extra profile id in the `extension_ids` keyword argument. +[maurits] diff --git a/news/3961.feature.1 b/news/3961.feature.1 new file mode 100644 index 0000000000..da7c79fa52 --- /dev/null +++ b/news/3961.feature.1 @@ -0,0 +1,3 @@ +You can pass a `distribution_name` to `factory.addPloneSite`. +We then pass all other arguments and keyword arguments to the `plone.distribution` site api. +[maurits]