From d4c226a6bc9ee29863d4da4a3c49f16b73848b70 Mon Sep 17 00:00:00 2001 From: Peter Holzer Date: Tue, 26 May 2026 13:52:38 +0200 Subject: [PATCH 01/11] Move plone.app.i18n.languageselector to plone.app.layout --- src/plone/app/layout/viewlets/common.py | 137 ++++++++++++++++++ src/plone/app/layout/viewlets/configure.zcml | 9 ++ .../app/layout/viewlets/languageselector.pt | 43 ++++++ .../viewlets/tests/test_languageselector.py | 16 ++ 4 files changed, 205 insertions(+) create mode 100644 src/plone/app/layout/viewlets/languageselector.pt create mode 100644 src/plone/app/layout/viewlets/tests/test_languageselector.py diff --git a/src/plone/app/layout/viewlets/common.py b/src/plone/app/layout/viewlets/common.py index e4e58d69..580230a2 100644 --- a/src/plone/app/layout/viewlets/common.py +++ b/src/plone/app/layout/viewlets/common.py @@ -170,6 +170,143 @@ def update(self): self.site_actions = context_state.actions("site_actions") +@implementer(IViewlet) +class LanguageSelectorViewlet(BrowserView): + """Language selector. + + >>> ls = LanguageSelectorViewlet(None, dict(), None, None) + >>> ls + + + >>> ls.update() + >>> ls.available() + False + >>> ls.languages() + [] + >>> ls.showFlags() + False + + >>> class Tool(object): + ... use_cookie_negotiation = False + ... supported_langs = ['de', 'en', 'ar'] + ... always_show_selector = False + ... + ... def __init__(self, **kw): + ... self.__dict__.update(kw) + ... self.showFlags = True + ... + ... def getSupportedLanguages(self): + ... return self.supported_langs + ... + ... def getAvailableLanguageInformation(self): + ... return dict(en={'selected' : True}, de={'selected' : False}, + ... nl={'selected' : True}, ar={'selected': True}) + ... + ... def getLanguageBindings(self, request): + ... # en = selected by user, nl = default, [] = other options + ... return ('en', 'nl', []) + ... + ... def showSelector(self): + ... return bool(self.use_cookie_negotiation or self.always_show_selector) + + >>> ls.tool = Tool() + >>> ls.available() + False + + >>> ls.tool = Tool(use_cookie_negotiation=True) + >>> ls.available() + True + + >>> result = [{'code': 'en', 'selected': True}, {'code': 'ar', 'selected': False}, + ... {'code': 'nl', 'selected': False}] + >>> ls.languages() == result + True + + >>> ls.showFlags() + True + + >>> ls.tool = Tool(use_cookie_negotiation=True) + >>> ls.available() + True + + >>> ls.tool = Tool(always_show_selector=True) + >>> ls.available() + True + + >>> from zope.interface import implementer + >>> from OFS.interfaces import IItem + >>> @implementer(IItem) + ... class Dummy(object): + ... def getPortalObject(self): + ... return self + ... def absolute_url(self): + ... return 'absolute url' + + >>> context = Dummy() + >>> context.portal_url = Dummy() + >>> ls = LanguageSelectorViewlet(context, dict(), None, None) + >>> ls.portal_url() + 'absolute url' + """ + + def __init__(self, context, request, view, manager): + super().__init__(context, request) + self.view = view + self.manager = manager + + def update(self): + self.tool = getToolByName(self.context, "portal_languages", None) + + def available(self) -> bool: + if self.tool is None: + return False + selector = self.tool.showSelector() + languages = len(self.tool.getSupportedLanguages()) > 1 + return selector and languages + + def portal_url(self): + portal_tool = getToolByName(self.context, "portal_url", None) + if portal_tool is not None: + return portal_tool.getPortalObject().absolute_url() + return None + + def languages(self): + """Returns list of languages.""" + if self.tool is None: + return [] + + bound = self.tool.getLanguageBindings(self.request) + current = bound[0] + + def merge(lang, info): + info["code"] = lang + info["selected"] = lang == current + return info + + languages = [ + merge(lang, info) + for (lang, info) in self.tool.getAvailableLanguageInformation().items() + if info["selected"] + ] + + # sort supported languages by index in portal_languages tool + supported_langs = self.tool.getSupportedLanguages() + + def index(info): + try: + return supported_langs.index(info["code"]) + except ValueError: + return len(supported_langs) + + return sorted(languages, key=index) + + def showFlags(self): + """Do we use flags?.""" + if self.tool is None: + return False + return self.tool.showFlags + + class SearchBoxViewlet(ViewletBase): index = ViewPageTemplateFile("searchbox.pt") diff --git a/src/plone/app/layout/viewlets/configure.zcml b/src/plone/app/layout/viewlets/configure.zcml index 8524b381..ae32c24f 100644 --- a/src/plone/app/layout/viewlets/configure.zcml +++ b/src/plone/app/layout/viewlets/configure.zcml @@ -148,6 +148,15 @@ permission="zope2.View" /> + + + + + diff --git a/src/plone/app/layout/viewlets/tests/test_languageselector.py b/src/plone/app/layout/viewlets/tests/test_languageselector.py new file mode 100644 index 00000000..4fac1063 --- /dev/null +++ b/src/plone/app/layout/viewlets/tests/test_languageselector.py @@ -0,0 +1,16 @@ +from zope.component.testing import setUp +from zope.component.testing import tearDown + +import doctest +import unittest + + +def test_suite(): + return unittest.TestSuite( + doctest.DocTestSuite( + "plone.app.i18n.locales.browser.selector", + setUp=setUp(), + tearDown=tearDown, + optionflags=doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE, + ) + ) From 6607d04b5bb7d5199550e5c6dd848e4cd966d65a Mon Sep 17 00:00:00 2001 From: Peter Holzer Date: Tue, 26 May 2026 14:36:11 +0200 Subject: [PATCH 02/11] Add news --- news/3953.feature.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/3953.feature.md diff --git a/news/3953.feature.md b/news/3953.feature.md new file mode 100644 index 00000000..9503bafe --- /dev/null +++ b/news/3953.feature.md @@ -0,0 +1,2 @@ +Moved plone.app.i18n.browser.languageselector template to plone.app.layout.viewlets +[agitator] \ No newline at end of file From c58cf54fd4e73134ebf65f638a46f031898c71fa Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Wed, 27 May 2026 12:28:20 +0200 Subject: [PATCH 03/11] Rename news snippet to breaking. This is for Plone 6.3 only, so it breaks compatibility with Plone 6.2. [ci skip] --- news/{3953.feature.md => 3953.breaking.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{3953.feature.md => 3953.breaking.md} (100%) diff --git a/news/3953.feature.md b/news/3953.breaking.md similarity index 100% rename from news/3953.feature.md rename to news/3953.breaking.md From 35dd5e77b541fe6f51ae3ced18758d96ef2be30c Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Wed, 27 May 2026 12:28:39 +0200 Subject: [PATCH 04/11] Bumped version for breaking release. [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4f5914ba..d8e608a6 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from pathlib import Path from setuptools import setup -version = "6.0.2.dev0" +version = "7.0.0a1.dev0" long_description = ( f"{Path('README.rst').read_text()}\n{Path('CHANGES.rst').read_text()}" From 4d0314002def5356687ed2ffb7afbb97f72766cf Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Wed, 27 May 2026 12:29:03 +0200 Subject: [PATCH 05/11] Preparing release 7.0.0a1 [ci skip] --- CHANGES.rst | 10 ++++++++++ news/3953.breaking.md | 2 -- setup.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 news/3953.breaking.md diff --git a/CHANGES.rst b/CHANGES.rst index f48cd9c8..5a99cb33 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,16 @@ Changelog .. towncrier release notes start +7.0.0a1 (2026-05-27) +-------------------- + +Breaking changes: + + +- Moved plone.app.i18n.browser.languageselector template to plone.app.layout.viewlets + [agitator] (#3953) + + 6.0.1 (2026-05-07) ------------------ diff --git a/news/3953.breaking.md b/news/3953.breaking.md deleted file mode 100644 index 9503bafe..00000000 --- a/news/3953.breaking.md +++ /dev/null @@ -1,2 +0,0 @@ -Moved plone.app.i18n.browser.languageselector template to plone.app.layout.viewlets -[agitator] \ No newline at end of file diff --git a/setup.py b/setup.py index d8e608a6..9978106b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from pathlib import Path from setuptools import setup -version = "7.0.0a1.dev0" +version = "7.0.0a1" long_description = ( f"{Path('README.rst').read_text()}\n{Path('CHANGES.rst').read_text()}" From 390cb903682d21fe059d5251fd84d354e626f166 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Wed, 27 May 2026 12:29:27 +0200 Subject: [PATCH 06/11] Back to development: 7.0.0a2 [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9978106b..9bc78815 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from pathlib import Path from setuptools import setup -version = "7.0.0a1" +version = "7.0.0a2.dev0" long_description = ( f"{Path('README.rst').read_text()}\n{Path('CHANGES.rst').read_text()}" From 4ac819e43835dd149758ba5e565126221552445c Mon Sep 17 00:00:00 2001 From: ale-rt Date: Sat, 30 May 2026 09:49:24 +0200 Subject: [PATCH 07/11] Configuring with plone.meta --- .github/workflows/test-matrix.yml | 4 ++-- .meta.toml | 4 ++-- news/+meta.internal | 2 ++ tox.ini | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 news/+meta.internal diff --git a/.github/workflows/test-matrix.yml b/.github/workflows/test-matrix.yml index 91785f4b..5c0b968c 100644 --- a/.github/workflows/test-matrix.yml +++ b/.github/workflows/test-matrix.yml @@ -20,8 +20,8 @@ jobs: - ["ubuntu", "ubuntu-latest"] config: # [Python version, visual name, tox env] - - ["3.14", "6.2 on py3.14", "py314-plone62"] - - ["3.10", "6.2 on py3.10", "py310-plone62"] + - ["3.14", "6.3 on py3.14", "py314-plone63"] + - ["3.10", "6.3 on py3.10", "py310-plone63"] runs-on: ${{ matrix.os[1] }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name diff --git a/.meta.toml b/.meta.toml index d64e921d..2c1f0446 100644 --- a/.meta.toml +++ b/.meta.toml @@ -3,7 +3,7 @@ # See the inline comments on how to expand/tweak this configuration file [meta] template = "default" -commit-id = "2.8.1.dev0" +commit-id = "2.9.0" [pyproject] dependencies_ignores = "['plone.app.relationfield', 'plone.app.multilingual', 'Products.CMFPlone']" @@ -14,4 +14,4 @@ extra_lines = """ """ [tox] -test_matrix = { "6.2" = ["*"] } +test_matrix = { "6.3" = ["*"] } diff --git a/news/+meta.internal b/news/+meta.internal new file mode 100644 index 00000000..c08f5399 --- /dev/null +++ b/news/+meta.internal @@ -0,0 +1,2 @@ +Update configuration files. +[plone devs] diff --git a/tox.ini b/tox.ini index 500e6796..08f95517 100644 --- a/tox.ini +++ b/tox.ini @@ -7,11 +7,11 @@ min_version = 4.4.0 envlist = lint test - py314-plone62 - py313-plone62 - py312-plone62 - py311-plone62 - py310-plone62 + py314-plone63 + py313-plone63 + py312-plone63 + py311-plone63 + py310-plone63 dependencies @@ -112,7 +112,7 @@ set_env = ## deps = {[test_runner]deps} - plone62: -c https://dist.plone.org/release/6.2-dev/constraints.txt + plone63: -c https://dist.plone.org/release/6.3-dev/constraints.txt ## # Specify additional deps in .meta.toml: @@ -153,7 +153,7 @@ constrain_package_deps = {[base]constrain_package_deps} set_env = {[base]set_env} deps = {[test_runner]deps} - -c https://dist.plone.org/release/6.2-dev/constraints.txt + -c https://dist.plone.org/release/6.3-dev/constraints.txt commands = {[test_runner]test} extras = {[base]extras} @@ -179,7 +179,7 @@ set_env = {[base]set_env} deps = {[test_runner]deps} coverage - -c https://dist.plone.org/release/6.2-dev/constraints.txt + -c https://dist.plone.org/release/6.3-dev/constraints.txt commands = {[test_runner]coverage} extras = {[base]extras} @@ -192,7 +192,7 @@ deps = twine build towncrier - -c https://dist.plone.org/release/6.2-dev/constraints.txt + -c https://dist.plone.org/release/6.3-dev/constraints.txt commands = # fake version to not have to install the package # we build the change log as news entries might break @@ -222,7 +222,7 @@ allowlist_externals = deps = pipdeptree pipforester - -c https://dist.plone.org/release/6.2-dev/constraints.txt + -c https://dist.plone.org/release/6.3-dev/constraints.txt commands = # Generate the full dependency tree sh -c 'pipdeptree -j > forest.json' From d8963f02621b7d1d5f495882a44834df361fdd2c Mon Sep 17 00:00:00 2001 From: Peter Mathis Date: Thu, 13 Feb 2025 08:45:31 +0100 Subject: [PATCH 08/11] Move linkintegrity Classic-UI templates/views to plone.app.layout Refs. https://github.com/plone/Products.CMFPlone/issues/3953 --- news/3953.breaking.1 | 2 + setup.py | 6 + .../layout/analytics/tests/test_doctests.py | 4 +- .../app/layout/controlpanels/configure.zcml | 17 ++ .../app/layout/controlpanels/linkintegrity.py | 76 +++++ .../linkintegrity_delete_confirmation_info.pt | 135 ++++++++ .../templates/linkintegrity_update.pt | 58 ++++ .../app/layout/globals/tests/test_context.py | 4 +- .../app/layout/globals/tests/test_layout.py | 6 +- .../app/layout/globals/tests/test_portal.py | 4 +- .../app/layout/globals/tests/test_tools.py | 4 +- .../layout/links/tests/test_canonical_url.py | 4 +- .../links/tests/test_favicon_viewlet.py | 4 +- .../app/layout/sitemap/tests/test_sitemap.py | 4 +- src/plone/app/layout/testing.py | 23 +- .../tests/robot/test_linkintegrity.robot | 132 ++++++++ .../app/layout/tests/test_confirm_view.py | 4 +- .../app/layout/tests/test_linkintegrity.py | 287 ++++++++++++++++++ src/plone/app/layout/tests/test_robot.py | 30 ++ src/plone/app/layout/tests/test_setup.py | 6 +- src/plone/app/layout/viewlets/tests/base.py | 8 +- .../layout/viewlets/tests/test_functional.py | 4 +- tox.ini | 2 + 23 files changed, 790 insertions(+), 34 deletions(-) create mode 100644 news/3953.breaking.1 create mode 100644 src/plone/app/layout/controlpanels/linkintegrity.py create mode 100644 src/plone/app/layout/controlpanels/templates/linkintegrity_delete_confirmation_info.pt create mode 100644 src/plone/app/layout/controlpanels/templates/linkintegrity_update.pt create mode 100644 src/plone/app/layout/tests/robot/test_linkintegrity.robot create mode 100644 src/plone/app/layout/tests/test_linkintegrity.py create mode 100644 src/plone/app/layout/tests/test_robot.py diff --git a/news/3953.breaking.1 b/news/3953.breaking.1 new file mode 100644 index 00000000..65a145bc --- /dev/null +++ b/news/3953.breaking.1 @@ -0,0 +1,2 @@ +Move ``plone.app.linkintegrity`` template/views to ``plone.app.layout.views.linkintegrity`` +[petschki] diff --git a/setup.py b/setup.py index 9bc78815..a7424451 100644 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ install_requires=[ "plone.app.content", "plone.app.dexterity", + "plone.app.linkintegrity", "plone.app.relationfield", "plone.app.uuid", "plone.app.viewletmanager >=1.2", @@ -61,13 +62,18 @@ extras_require=dict( test=[ "plone.app.contenttypes[test]", + "plone.app.linkintegrity", "plone.app.relationfield", + "plone.app.robotframework", "plone.app.testing", + "plone.app.textfield", "plone.browserlayer", "plone.dexterity", "plone.locking", "plone.testing", + "robotsuite", "z3c.relationfield", + "zc.relation", "zope.intid", ] ), diff --git a/src/plone/app/layout/analytics/tests/test_doctests.py b/src/plone/app/layout/analytics/tests/test_doctests.py index f7f61d79..83126702 100644 --- a/src/plone/app/layout/analytics/tests/test_doctests.py +++ b/src/plone/app/layout/analytics/tests/test_doctests.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import FUNCTIONAL_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING from plone.testing import layered import doctest @@ -22,7 +22,7 @@ def test_suite(): test, optionflags=OPTIONFLAGS, ), - layer=FUNCTIONAL_TESTING, + layer=PLONE_APP_LAYOUT_FUNCTIONAL_TESTING, ) for test in normal_testfiles ] diff --git a/src/plone/app/layout/controlpanels/configure.zcml b/src/plone/app/layout/controlpanels/configure.zcml index ecacd150..370d65c1 100644 --- a/src/plone/app/layout/controlpanels/configure.zcml +++ b/src/plone/app/layout/controlpanels/configure.zcml @@ -4,4 +4,21 @@ xmlns:zcml="http://namespaces.zope.org/zcml" > + + + + diff --git a/src/plone/app/layout/controlpanels/linkintegrity.py b/src/plone/app/layout/controlpanels/linkintegrity.py new file mode 100644 index 00000000..e7cac3db --- /dev/null +++ b/src/plone/app/layout/controlpanels/linkintegrity.py @@ -0,0 +1,76 @@ +from Acquisition import aq_inner +from datetime import datetime +from datetime import timedelta +from plone.app.linkintegrity.browser.info import ( + DeleteConfirmationInfo as DeleteConfirmationInfoAPI, +) +from plone.app.linkintegrity.handlers import modifiedContent +from plone.base import PloneMessageFactory as _ +from Products.CMFCore.utils import getToolByName +from Products.Five import BrowserView +from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile +from Products.statusmessages.interfaces import IStatusMessage +from transaction import savepoint +from zExceptions import NotFound + +import logging + +logger = logging.getLogger(__name__) + + +class DeleteConfirmationInfo(DeleteConfirmationInfoAPI): + template = ViewPageTemplateFile( + "templates/linkintegrity_delete_confirmation_info.pt" + ) + + +class UpdateView(BrowserView): + """Iterate over all catalogued items and update linkintegrity-information.""" + + def __call__(self): + context = aq_inner(self.context) + request = aq_inner(self.request) + if "update" in request.form or "delete_all" in request.form: + starttime = datetime.now() + count = self.update() + duration = timedelta(seconds=(datetime.now() - starttime).seconds) + msg = _( + "linkintegrity_update_info", + default="Link integrity information updated for ${count} " + + "items in ${time} seconds.", + mapping={"count": count, "time": str(duration)}, + ) + IStatusMessage(request).add(msg, type="info") + msg = "Updated {} items in {} seconds".format( + count, + str(duration), + ) + logger.info(msg) + request.RESPONSE.redirect(getToolByName(context, "portal_url")()) + elif "cancel" in request.form: + msg = _("Update cancelled.") + IStatusMessage(request).add(msg, type="info") + request.RESPONSE.redirect(getToolByName(context, "portal_url")()) + else: + return self.index() + + def update(self): + catalog = getToolByName(self.context, "portal_catalog") + count = 0 + + for brain in catalog(): + try: + obj = brain.getObject() + except (AttributeError, NotFound, KeyError): + msg = "Catalog inconsistency: {0} not found!" + logger.error(msg.format(brain.getPath()), exc_info=1) + continue + try: + modifiedContent(obj, "dummy event parameter") + count += 1 + except Exception: + msg = "Error updating linkintegrity-info for {0}." + logger.error(msg.format(obj.absolute_url()), exc_info=1) + if count % 1000 == 0: + savepoint(optimistic=True) + return count diff --git a/src/plone/app/layout/controlpanels/templates/linkintegrity_delete_confirmation_info.pt b/src/plone/app/layout/controlpanels/templates/linkintegrity_delete_confirmation_info.pt new file mode 100644 index 00000000..0b938c06 --- /dev/null +++ b/src/plone/app/layout/controlpanels/templates/linkintegrity_delete_confirmation_info.pt @@ -0,0 +1,135 @@ + + + +

Potential link breakage

+ +
+ +

+ By deleting this item, you will break links that exist in the items listed + below. If this is indeed what you want to do, we recommend that you remove + these references first. +

+ +
+
+ + +
+

+ + This + + is referenced by the following items: + +

+
+ + + +
+ +
+ +

Deleting overview

+

+ + Number of selected, non-empty folders: + + +

+
    +
  • + + Following content within + + will also be deleted: +
    +
      +
    • + + +
    • +
    +
  • +
+ +
+ +

+ Would you like to delete it anyway? +

+ +
+ +
+ + + diff --git a/src/plone/app/layout/controlpanels/templates/linkintegrity_update.pt b/src/plone/app/layout/controlpanels/templates/linkintegrity_update.pt new file mode 100644 index 00000000..f099245c --- /dev/null +++ b/src/plone/app/layout/controlpanels/templates/linkintegrity_update.pt @@ -0,0 +1,58 @@ + + + + + + + + + +

Update link integrity information

+ +
+ +

+ Clicking the below button will cause link integrity information to be + updated. This might take a while, especially for bigger sites... +

+ +
+ + +
+ +
+ +
+ + diff --git a/src/plone/app/layout/globals/tests/test_context.py b/src/plone/app/layout/globals/tests/test_context.py index 7afc2d62..89b04e5a 100644 --- a/src/plone/app/layout/globals/tests/test_context.py +++ b/src/plone/app/layout/globals/tests/test_context.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID from plone.app.testing.helpers import logout @@ -16,7 +16,7 @@ class TestContextStateView(unittest.TestCase): """Ensure that the basic redirector setup is successful.""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.app = self.layer["app"] diff --git a/src/plone/app/layout/globals/tests/test_layout.py b/src/plone/app/layout/globals/tests/test_layout.py index a0e8d3fa..632bb139 100644 --- a/src/plone/app/layout/globals/tests/test_layout.py +++ b/src/plone/app/layout/globals/tests/test_layout.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID from plone.app.testing.helpers import logout @@ -17,7 +17,7 @@ class TestLayoutView(unittest.TestCase): """Tests the global layout view.""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] @@ -196,7 +196,7 @@ def testBodyClassColMarker(self): class TestLayoutViewXHR(unittest.TestCase): """Tests the global layout view with it's XHR utilities.""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/globals/tests/test_portal.py b/src/plone/app/layout/globals/tests/test_portal.py index c9adf624..fea289eb 100644 --- a/src/plone/app/layout/globals/tests/test_portal.py +++ b/src/plone/app/layout/globals/tests/test_portal.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID from plone.app.testing.helpers import logout @@ -20,7 +20,7 @@ class TestPortalStateView(unittest.TestCase): """Ensure that the basic redirector setup is successful.""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/globals/tests/test_tools.py b/src/plone/app/layout/globals/tests/test_tools.py index aeb464b2..702e228d 100644 --- a/src/plone/app/layout/globals/tests/test_tools.py +++ b/src/plone/app/layout/globals/tests/test_tools.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from Products.CMFCore.utils import getToolByName import unittest @@ -7,7 +7,7 @@ class TestToolsView(unittest.TestCase): """Tests the global tools view.""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/links/tests/test_canonical_url.py b/src/plone/app/layout/links/tests/test_canonical_url.py index b20ba6ac..5579ab2b 100644 --- a/src/plone/app/layout/links/tests/test_canonical_url.py +++ b/src/plone/app/layout/links/tests/test_canonical_url.py @@ -1,11 +1,11 @@ -from plone.app.layout.testing import FUNCTIONAL_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING from plone.testing.zope import Browser import unittest class ViewletTestCase(unittest.TestCase): - layer = FUNCTIONAL_TESTING + layer = PLONE_APP_LAYOUT_FUNCTIONAL_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/links/tests/test_favicon_viewlet.py b/src/plone/app/layout/links/tests/test_favicon_viewlet.py index c05c5d23..c67234e0 100644 --- a/src/plone/app/layout/links/tests/test_favicon_viewlet.py +++ b/src/plone/app/layout/links/tests/test_favicon_viewlet.py @@ -1,5 +1,5 @@ from plone.app.layout.links.viewlets import FaviconViewlet -from plone.app.layout.testing import FUNCTIONAL_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING from plone.app.layout.viewlets.tests.base import ViewletsTestCase from plone.base.interfaces import ISiteSchema from plone.formwidget.namedfile.converter import b64encode_file @@ -8,7 +8,7 @@ class TestFaviconViewletView(ViewletsTestCase, FaviconViewlet): - layer = FUNCTIONAL_TESTING + layer = PLONE_APP_LAYOUT_FUNCTIONAL_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/sitemap/tests/test_sitemap.py b/src/plone/app/layout/sitemap/tests/test_sitemap.py index 4952cd08..1b2ccfc9 100644 --- a/src/plone/app/layout/sitemap/tests/test_sitemap.py +++ b/src/plone/app/layout/sitemap/tests/test_sitemap.py @@ -1,7 +1,7 @@ from DateTime import DateTime from gzip import GzipFile from io import BytesIO -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.app.testing import login from plone.app.testing import logout from plone.app.testing import setRoles @@ -25,7 +25,7 @@ class SiteMapTestCase(unittest.TestCase): """base test case with convenience methods for all sitemap tests""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/testing.py b/src/plone/app/layout/testing.py index f9f56cbd..6101bb85 100644 --- a/src/plone/app/layout/testing.py +++ b/src/plone/app/layout/testing.py @@ -1,13 +1,15 @@ from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE +from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE from plone.app.testing import applyProfile from plone.app.testing import FunctionalTesting from plone.app.testing import IntegrationTesting from plone.app.testing import PloneSandboxLayer from plone.app.testing import TEST_USER_ID from plone.base.utils import unrestricted_construct_instance +from plone.testing import zope -class Fixture(PloneSandboxLayer): +class PloneAppLayoutFixture(PloneSandboxLayer): defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,) def setUpZope(self, app, configurationContext): @@ -27,12 +29,21 @@ def setUpPloneSite(self, portal): applyProfile(portal, "plone.app.layout:default") -FIXTURE = Fixture() -INTEGRATION_TESTING = IntegrationTesting( - bases=(FIXTURE,), +class PloneAppLayoutRemoteFixture(PloneAppLayoutFixture): + defaultBases = (REMOTE_LIBRARY_BUNDLE_FIXTURE,) + + +PLONE_APP_LAYOUT_FIXTURE = PloneAppLayoutFixture() +PLONE_APP_LAYOUT_INTEGRATION_TESTING = IntegrationTesting( + bases=(PLONE_APP_LAYOUT_FIXTURE,), name="plone.app.layout:Integration", ) -FUNCTIONAL_TESTING = FunctionalTesting( - bases=(FIXTURE,), +PLONE_APP_LAYOUT_FUNCTIONAL_TESTING = FunctionalTesting( + bases=(PLONE_APP_LAYOUT_FIXTURE,), name="plone.app.layout:Functional", ) +PLONE_APP_LAYOUT_ROBOT_FIXTURE = PloneAppLayoutRemoteFixture() +PLONE_APP_LAYOUT_ROBOT_TESTING = FunctionalTesting( + bases=(PLONE_APP_LAYOUT_ROBOT_FIXTURE, zope.WSGI_SERVER_FIXTURE), + name="plone.app.layout:Robot", +) diff --git a/src/plone/app/layout/tests/robot/test_linkintegrity.robot b/src/plone/app/layout/tests/robot/test_linkintegrity.robot new file mode 100644 index 00000000..65c6cf4b --- /dev/null +++ b/src/plone/app/layout/tests/robot/test_linkintegrity.robot @@ -0,0 +1,132 @@ +*** Settings *** + +Resource plone/app/robotframework/browser.robot +Resource Products/CMFPlone/tests/robot/keywords.robot + +Library Remote ${PLONE_URL}/RobotRemote + +Test Setup Run Keywords Plone test setup +Test Teardown Run keywords Plone test teardown + + +*** Test Cases *** + +Scenario: When page is linked show warning + Given a logged-in site administrator + and a page to link to + and a page to edit + When I add a link in rich text + Then I should see a warning when deleting page + +Scenario: After you fix linked page no longer show warning + Given a logged-in site administrator + and a page to link to + and a page to edit + When I add a link in rich text + Then I should see a warning when deleting page + + When I remove link to page + Then I should not see a warning when deleting page + +Scenario: Show warning when deleting linked item from folder_contents + Given a logged-in site administrator + and a page to link to + and a page to edit + When I add a link in rich text + Then I should see a warning when deleting page from folder_contents + + When I remove link to page + Then I should not see a warning when deleting page from folder_contents + +*** Keywords *** + +# GIVEN + +a page to link to + Create content + ... type=Document + ... id=foo + ... title=Foo + +a page to edit + Create content + ... type=Document + ... id=bar + ... title=Bar + +# When +I add a link in rich text + Go To ${PLONE_URL}/bar/edit + Fill text to tinymce editor foo + Mark text foo in tinymce editor + Click //button[@aria-label="Insert/edit link"] + Click //div[contains(@class,"linkModal")]//div[contains(@class,"content-browser-selected-items-wrapper")]//a[contains(@class,"btn-primary")] + Click item in contenbrowser column 1 3 + Click //div[contains(@class, "content-browser-wrapper")]//div[contains(@class, "levelColumns")]/div[contains(@class, "preview")]/div[contains(@class, "levelToolbar")]//button + Click //div[contains(@class,"modal-footer")]//input[@name="insert"] + Click //button[@name="form.buttons.save"] + Wait For Condition Text //body contains Changes saved + +I remove link to page + Go To ${PLONE_URL}/bar + Click //*[@id="contentview-edit"]//a + Fill text to tinymce editor foo + Mark text foo in tinymce editor + Click //button[@aria-label="Remove link"] + Click //button[@name="form.buttons.save"] + Wait For Condition Text //body contains Changes saved + +# Then + +I should see a warning when deleting page + Go To ${PLONE_URL}/foo + Click //*[@id="plone-contentmenu-actions"]/a + Click //*[@id="plone-contentmenu-actions-delete"] + Get Element Count //*[contains(@class,"breach-container")]//*[contains(@class,"breach-item")] greater than 0 + +I should not see a warning when deleting page + Go To ${PLONE_URL}/foo + Click //*[@id="plone-contentmenu-actions"]/a + Click //*[@id="plone-contentmenu-actions-delete"] + Get Element Count //*[contains(@class,"breach-container")]//*[contains(@class,"breach-item")] should be 0 + + +I should see a warning when deleting page from folder_contents + Go To ${PLONE_URL}/folder_contents + Check Checkbox //tr[@data-id="foo"]//input + Get Checkbox State //tr[@data-id="foo"]//input == checked + Get Element Count //*[@id="btngroup-mainbuttons"]//a[@id="btn-delete" and contains(@class,"disabled")] should be 0 + Click //*[@id="btngroup-mainbuttons"]//a[@id="btn-delete"] + Get Element Count //*[contains(@class,"breach-container")]//*[contains(@class,"breach-item")] greater than 0 + Get Checkbox State //tr[@data-id="foo"]//input == checked + + +I should not see a warning when deleting page from folder_contents + Go To ${PLONE_URL}/folder_contents + Check Checkbox //tr[@data-id="foo"]//input + Get Checkbox State //tr[@data-id="foo"]//input == checked + Get Element Count //*[@id="btngroup-mainbuttons"]//a[@id="btn-delete" and contains(@class,"disabled")] should be 0 + Click //*[@id="btngroup-mainbuttons"]//a[@id="btn-delete"] + Get Element States //*[@id="popover-delete"]//*[contains(@class,"popover-content")] contains visible + Get Element Count //*[contains(@class,"breach-container")]//*[contains(@class,"breach-item")] should be 0 + Click //*[contains(@class,"popover-content")]//button[contains(@class,"applyBtn")] + Wait For Condition Text //body contains Successfully delete items + Get Element Count //tr[@data-id="foo"]//input should be 0 + +# DRY + +Mark text foo in tinymce editor + + # select the text `heading` via javascript + Evaluate JavaScript ${None} + ... () => { + ... let iframe_document = document.querySelector(".tox-edit-area iframe").contentDocument; + ... let body = iframe_document.body; + ... let p = body.firstChild; + ... let range = new Range(); + ... range.setStart(p.firstChild, 0); + ... range.setEnd(p.firstChild, 3); + ... iframe_document.getSelection().removeAllRanges(); + ... iframe_document.getSelection().addRange(range); + ... } + ... all_elements=False diff --git a/src/plone/app/layout/tests/test_confirm_view.py b/src/plone/app/layout/tests/test_confirm_view.py index 5628f104..ebcaac9b 100644 --- a/src/plone/app/layout/tests/test_confirm_view.py +++ b/src/plone/app/layout/tests/test_confirm_view.py @@ -1,11 +1,11 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from zope.component import getMultiAdapter import unittest class TestAttackVector(unittest.TestCase): - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def test_using_correct_template(self): """Ensure that confirm-action view uses the confirm.pt template from plone.app.layout.""" diff --git a/src/plone/app/layout/tests/test_linkintegrity.py b/src/plone/app/layout/tests/test_linkintegrity.py new file mode 100644 index 00000000..bbff984d --- /dev/null +++ b/src/plone/app/layout/tests/test_linkintegrity.py @@ -0,0 +1,287 @@ +"""Functional tests for link integrity HTML rendering. + +These tests verify that the delete confirmation page correctly renders +link integrity warnings (HTML). The underlying API logic (get_breaches etc.) +is tested in plone.app.linkintegrity. +""" + +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING +from plone.app.linkintegrity.testing import create +from plone.app.linkintegrity.testing import GIF +from plone.app.linkintegrity.utils import getIncomingLinks +from plone.app.linkintegrity.utils import getOutgoingLinks +from plone.app.testing import login +from plone.app.testing import setRoles +from plone.app.testing import TEST_USER_ID +from plone.app.testing import TEST_USER_NAME +from plone.app.testing import TEST_USER_PASSWORD +from plone.app.textfield import RichTextValue +from plone.base.interfaces import IEditingSchema +from plone.registry.interfaces import IRegistry +from plone.testing.zope import Browser +from zc.relation.interfaces import ICatalog +from zope.component import getMultiAdapter +from zope.component import getUtility +from zope.lifecycleevent import modified + +import re +import transaction +import unittest + + +def set_text(obj, text): + obj.text = RichTextValue(text) + modified(obj) + + +class LinkIntegrityFunctionalTestCase(unittest.TestCase): + """Functional tests for link integrity HTML rendering in delete confirmation.""" + + layer = PLONE_APP_LAYOUT_FUNCTIONAL_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + login(self.portal, TEST_USER_NAME) + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + # Create sample content + for i in range(1, 4): + create(self.portal, "Document", id=f"doc{i}", title=f"Test Page {i}") + create(self.portal, "File", id="file1", title="File 1", file=GIF) + create(self.portal, "Folder", id="folder1", title="Folder 1") + create(self.portal["folder1"], "Document", id="doc4", title="Test Page 4") + + # Get a testbrowser + self.browser = Browser(self.layer["app"]) + self.browser.handleErrors = False + self.browser.addHeader("Referer", self.portal.absolute_url()) + self.browser.addHeader( + "Authorization", f"Basic {TEST_USER_NAME}:{TEST_USER_PASSWORD}" + ) + + # Initial page load to compile bundles before rendering exception views + transaction.commit() + self.browser.open(self.portal.absolute_url()) + + def _get_token(self, obj): + return getMultiAdapter((obj, self.request), name="authenticator").token() + + def test_file_reference_linkintegrity_page_is_shown(self): + doc1 = self.portal.doc1 + file2 = create(self.portal, "File", id="file2", file=GIF) + + set_text(doc1, 'A File') + token = self._get_token(file2) + self.request["_authenticator"] = token + transaction.commit() + + self.browser.handleErrors = True + delete_url = "{}/delete_confirmation?_authenticator={}".format( + file2.absolute_url(), token + ) + self.browser.open(delete_url) + self.assertIn("Potential link breakage", self.browser.contents) + self.assertIn( + 'Test Page 1', self.browser.contents + ) + self.assertIn("Would you like to delete it anyway?", self.browser.contents) + + self.browser.getControl(name="form.buttons.Delete").click() + self.assertNotIn("file2", self.portal.objectIds()) + + def test_renaming_referenced_item(self): + doc1 = self.portal.doc1 + doc2 = self.portal.doc2 + + set_text(doc1, 'doc2') + self.assertEqual([i.from_object for i in getIncomingLinks(doc2)], [doc1]) + transaction.commit() + + self.browser.handleErrors = True + self.browser.open( + "{}/object_rename?_authenticator={}".format( + doc1.absolute_url(), self._get_token(doc1) + ) + ) + self.browser.getControl(name="form.widgets.new_id").value = "nuname" + self.browser.getControl(name="form.buttons.Rename").click() + self.assertIn("Renamed 'doc1' to 'nuname'.", self.browser.contents) + transaction.commit() + + self.assertIn(doc1, [i.from_object for i in getIncomingLinks(doc2)]) + self.browser.open(doc2.absolute_url()) + self.browser.getLink("Delete").click() + self.assertIn("Potential link breakage", self.browser.contents) + self.assertIn( + 'Test Page 1', + self.browser.contents, + ) + self.browser.getControl(name="form.buttons.Delete").click() + self.assertNotIn("doc2", self.portal.objectIds()) + + def test_removal_in_subfolder(self): + doc1 = self.portal.doc1 + doc2 = self.portal.doc2 + folder1 = self.portal.folder1 + + set_text(doc1, 'a document') + set_text(doc2, 'a document') + transaction.commit() + + self.browser.handleErrors = True + self.browser.open( + "{}/delete_confirmation?_authenticator={}".format( + folder1.absolute_url(), self._get_token(folder1) + ) + ) + self.assertIn("Potential link breakage", self.browser.contents) + self.assertIn( + 'Test Page 1', self.browser.contents + ) + self.assertIn( + 'Test Page 2', self.browser.contents + ) + self.browser.getControl(name="form.buttons.Delete").click() + self.assertNotIn("folder1", self.portal.objectIds()) + + def test_removal_with_cookie_auth(self): + doc1 = self.portal.doc1 + doc2 = self.portal.doc2 + + set_text(doc1, 'doc2') + transaction.commit() + + browser = Browser(self.layer["app"]) + browser.handleErrors = True + browser.addHeader("Referer", self.portal.absolute_url()) + browser.open(f"{self.portal.absolute_url()}/folder_contents") + self.assertIn("login?came_from", browser.url) + + browser.getControl(name="__ac_name").value = TEST_USER_NAME + browser.getControl(name="__ac_password").value = TEST_USER_PASSWORD + browser.getControl("Log in").click() + self.assertNotIn("authorization", [h.lower() for h in browser.headers.keys()]) + + browser.open( + "{}/delete_confirmation?_authenticator={}".format( + doc2.absolute_url(), self._get_token(doc2) + ) + ) + self.assertIn("Potential link breakage", browser.contents) + self.assertIn( + 'Test Page 1', browser.contents + ) + browser.getControl(name="form.buttons.Delete").click() + self.assertNotIn("doc2", self.portal.objectIds()) + + def test_linkintegrity_on_off_switch(self): + doc1 = self.portal.doc1 + doc2 = self.portal.doc2 + + set_text(doc1, 'a document') + transaction.commit() + + self.browser.handleErrors = True + self.browser.open( + "{}/delete_confirmation?_authenticator={}".format( + doc2.absolute_url(), self._get_token(doc2) + ) + ) + self.assertIn("Potential link breakage", self.browser.contents) + + registry = getUtility(IRegistry) + settings = registry.forInterface(IEditingSchema, prefix="plone") + settings.enable_link_integrity_checks = False + transaction.commit() + self.browser.reload() + self.assertNotIn("Potential link breakage", self.browser.contents) + + def test_references_on_cloned_objects(self): + doc1 = self.portal.doc1 + doc2 = self.portal.doc2 + + set_text(doc1, 'a document') + token = self._get_token(doc1) + self.request["_authenticator"] = token + doc1.restrictedTraverse("object_copy")() + self.request["_authenticator"] = token + self.portal.restrictedTraverse("object_paste")() + self.assertIn("copy_of_doc1", self.portal) + transaction.commit() + + self.browser.handleErrors = True + self.browser.open( + "{}/delete_confirmation?_authenticator={}".format( + doc2.absolute_url(), self._get_token(doc2) + ) + ) + self.assertIn("Potential link breakage", self.browser.contents) + self.assertIn( + 'Test Page 1', self.browser.contents + ) + self.assertIn( + 'a document') + self.assertEqual([i.to_object for i in getOutgoingLinks(doc1)], [spaces1]) + transaction.commit() + + self.browser.handleErrors = True + self.browser.open( + "{}/delete_confirmation?_authenticator={}".format( + spaces1.absolute_url(), self._get_token(spaces1) + ) + ) + self.assertIn("Potential link breakage", self.browser.contents) + self.assertIn( + 'Test Page 1', self.browser.contents + ) + self.browser.getControl(name="form.buttons.Delete").click() + self.assertNotIn("some spaces.doc", self.portal.objectIds()) + + def test_warn_about_content(self): + folder1 = self.portal.folder1 + self.browser.open( + "{}/delete_confirmation?_authenticator={}".format( + folder1.absolute_url(), self._get_token(folder1) + ) + ) + self.assertIn("Number of selected", self.browser.contents) + self.assertTrue(re.search(r"2\s+Objects in all", self.browser.contents)) + self.assertTrue(re.search(r"1\s+Folders", self.browser.contents)) + self.assertTrue(re.search(r"0\s+Published objects", self.browser.contents)) + + def test_update(self): + doc1 = self.portal.doc1 + doc2 = self.portal.doc2 + doc4 = self.portal.folder1.doc4 + + set_text(doc1, 'a document') + set_text(doc2, 'a document') + + catalog = getUtility(ICatalog) + for rel in list(catalog.findRelations()): + catalog.unindex(rel) + + self.assertEqual([i.to_object for i in getOutgoingLinks(doc1)], []) + self.assertEqual([i.to_object for i in getOutgoingLinks(doc2)], []) + + transaction.commit() + self.browser.open( + f"{self.portal.absolute_url()}/updateLinkIntegrityInformation" + ) + self.browser.getControl("Update").click() + self.assertIn("Link integrity information updated for", self.browser.contents) + + self.assertEqual([i.to_object for i in getOutgoingLinks(doc1)], [doc2]) + self.assertEqual([i.to_object for i in getOutgoingLinks(doc2)], [doc4]) diff --git a/src/plone/app/layout/tests/test_robot.py b/src/plone/app/layout/tests/test_robot.py new file mode 100644 index 00000000..8f8a1bbc --- /dev/null +++ b/src/plone/app/layout/tests/test_robot.py @@ -0,0 +1,30 @@ +from plone.app.layout.testing import PLONE_APP_LAYOUT_ROBOT_TESTING +from plone.app.testing import ROBOT_TEST_LEVEL +from plone.testing import layered + +import os +import robotsuite +import unittest + + +def test_suite(): + suite = unittest.TestSuite() + current_dir = os.path.abspath(os.path.dirname(__file__)) + robot_dir = os.path.join(current_dir, "robot") + robot_tests = [ + os.path.join("robot", doc) + for doc in os.listdir(robot_dir) + if doc.endswith(".robot") and doc.startswith("test_") + ] + for robot_test in robot_tests: + robottestsuite = robotsuite.RobotTestSuite( + robot_test, + noncritical=["unstable"], + ) + robottestsuite.level = ROBOT_TEST_LEVEL + suite.addTests( + [ + layered(robottestsuite, layer=PLONE_APP_LAYOUT_ROBOT_TESTING), + ] + ) + return suite diff --git a/src/plone/app/layout/tests/test_setup.py b/src/plone/app/layout/tests/test_setup.py index 36107b4d..f91b08a3 100644 --- a/src/plone/app/layout/tests/test_setup.py +++ b/src/plone/app/layout/tests/test_setup.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.base.utils import get_installer import unittest @@ -7,7 +7,7 @@ class TestSetup(unittest.TestCase): """Test plone.app.layout setup.""" - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] @@ -21,7 +21,7 @@ def test_browserlayer(self): class TestUninstall(unittest.TestCase): - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/viewlets/tests/base.py b/src/plone/app/layout/viewlets/tests/base.py index 53c4c7d9..8a040027 100644 --- a/src/plone/app/layout/viewlets/tests/base.py +++ b/src/plone/app/layout/viewlets/tests/base.py @@ -1,12 +1,12 @@ -from plone.app.layout.testing import FUNCTIONAL_TESTING -from plone.app.layout.testing import INTEGRATION_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_INTEGRATION_TESTING from plone.app.layout.testing import TEST_USER_ID import unittest class ViewletsTestCase(unittest.TestCase): - layer = INTEGRATION_TESTING + layer = PLONE_APP_LAYOUT_INTEGRATION_TESTING def setUp(self): self.portal = self.layer["portal"] @@ -15,7 +15,7 @@ def setUp(self): class ViewletsFunctionalTestCase(unittest.TestCase): - layer = FUNCTIONAL_TESTING + layer = PLONE_APP_LAYOUT_FUNCTIONAL_TESTING def setUp(self): self.portal = self.layer["portal"] diff --git a/src/plone/app/layout/viewlets/tests/test_functional.py b/src/plone/app/layout/viewlets/tests/test_functional.py index c6924c6d..a3343567 100644 --- a/src/plone/app/layout/viewlets/tests/test_functional.py +++ b/src/plone/app/layout/viewlets/tests/test_functional.py @@ -1,4 +1,4 @@ -from plone.app.layout.testing import FUNCTIONAL_TESTING +from plone.app.layout.testing import PLONE_APP_LAYOUT_FUNCTIONAL_TESTING from plone.testing import layered import doctest @@ -19,7 +19,7 @@ def test_suite(): test, optionflags=optionflags, ), - layer=FUNCTIONAL_TESTING, + layer=PLONE_APP_LAYOUT_FUNCTIONAL_TESTING, ) for test in normal_testfiles ] diff --git a/tox.ini b/tox.ini index 08f95517..f83d447c 100644 --- a/tox.ini +++ b/tox.ini @@ -84,8 +84,10 @@ commands = [test_runner] deps = zope.testrunner test = + rfbrowser init zope-testrunner --all --test-path={toxinidir}/src -s plone.app.layout {posargs} coverage = + rfbrowser init coverage run --branch --source plone.app.layout {envbindir}/zope-testrunner --quiet --all --test-path={toxinidir}/src -s plone.app.layout {posargs} coverage report -m --format markdown coverage xml From ba4040652c70475d78bc2478f719af24b08add9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:19:17 +0000 Subject: [PATCH 09/11] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black-pre-commit-mirror: 26.3.1 → 26.5.1](https://github.com/psf/black-pre-commit-mirror/compare/26.3.1...26.5.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a9920aa..3eb4d6a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.3.1 + rev: 26.5.1 hooks: - id: black - repo: https://github.com/collective/zpretty From 82b52faee2abc80b7da4ff49d6fd5c38112f2bc1 Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 2 Jun 2026 11:59:32 +0200 Subject: [PATCH 10/11] Take into account the value of request parameter page for caching the generated sitemap --- news/+sitemapcache.fix | 1 + src/plone/app/layout/sitemap/sitemap.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/+sitemapcache.fix diff --git a/news/+sitemapcache.fix b/news/+sitemapcache.fix new file mode 100644 index 00000000..24886227 --- /dev/null +++ b/news/+sitemapcache.fix @@ -0,0 +1 @@ +Take into account the value of request parameter page for caching the generated sitemap @erral diff --git a/src/plone/app/layout/sitemap/sitemap.py b/src/plone/app/layout/sitemap/sitemap.py index 54da06f3..4ec4b7dc 100644 --- a/src/plone/app/layout/sitemap/sitemap.py +++ b/src/plone/app/layout/sitemap/sitemap.py @@ -24,7 +24,8 @@ def _render_cachekey(fun, self): url = self.context.absolute_url() catalog = getToolByName(self.context, "portal_catalog") counter = catalog.getCounter() - return f"{url}/{self.filename}/{counter}" + page = self.request.get("page", 0) + return f"{url}/{self.filename}/{counter}/{page}" class SiteMapView(BrowserView): From aa66e55cb7597d44147e3058c19803977d76954d Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Tue, 2 Jun 2026 18:29:55 +0200 Subject: [PATCH 11/11] rename changelog --- news/{+sitemapcache.fix => +sitemapcache.bugfix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{+sitemapcache.fix => +sitemapcache.bugfix} (100%) diff --git a/news/+sitemapcache.fix b/news/+sitemapcache.bugfix similarity index 100% rename from news/+sitemapcache.fix rename to news/+sitemapcache.bugfix