From 8fe9f3414729e6b3b9742557b2e517594c376593 Mon Sep 17 00:00:00 2001 From: stevepiercy Date: Mon, 9 Dec 2024 13:59:52 -0800 Subject: [PATCH] [fc] Repository: plone.api Branch: refs/heads/master Date: 2024-12-06T22:46:21-08:00 Author: Steve Piercy (stevepiercy) Commit: https://github.com/plone/plone.api/commit/5c7e0feaed0e5c8537953985e7aabe0b542b5efc Remove references to non-functional Coveralls Coveralls has not worked since Travis CI was removed back in 2021(?). Documentation still shows it is being used. It hasn't been missed. - Closes #537 - See #233, which I think should be closed. Files changed: M README.md M docs/contribute.md Repository: plone.api Branch: refs/heads/master Date: 2024-12-06T22:48:12-08:00 Author: Steve Piercy (stevepiercy) Commit: https://github.com/plone/plone.api/commit/86ae02909232d9d366cb4e13db4515525fef487a news Files changed: A news/543.documentation Repository: plone.api Branch: refs/heads/master Date: 2024-12-09T13:59:52-08:00 Author: Steve Piercy (stevepiercy) Commit: https://github.com/plone/plone.api/commit/a6145e06a8a6d4c7fd68f0b87ddea68213d4a979 Merge pull request #543 from plone/remove-unused-coveralls Remove references to non-functional Coveralls Files changed: A news/543.documentation M README.md M docs/contribute.md --- last_commit.txt | 86 ++++++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index f0c12249f3..7e2c668548 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,55 +1,55 @@ -Repository: Products.CMFPlone - - -Branch: refs/heads/6.0.x -Date: 2024-12-06T06:32:22+01:00 -Author: Peter Mathis (petschki) -Commit: https://github.com/plone/Products.CMFPlone/commit/2d0e821d4fb804e1d7c4ea8056e33e049f81322a - -Allow bundles to be rendered after all other. - -JS and CSS resources can now be rendered after all other resources in their -resource group including the theme (e.g. the Barceloneta theme CSS). - -There is an exception for custom CSS which can be defined in the theming -controlpanel. This one is always rendered as last style resource. - -To render resources after all others, give them the "depends" value of "all". -For each of these resources, "all" indicates that the resource depends on all other resources, making it render after its dependencies. -If you set multiple resources with "all", then they will render alphabetically after all other. - -This lets you override a theme with custom CSS from a bundle instead of having -to add the CSS customizations to the registry via the "custom_css" settings. -As a consequence, theme customization can now be done in the filesystem in -ordinary CSS files instead of being bound to a time consuming workflow which -involves upgrading the custom_css registry after every change. - - -Co-authored-by: Johannes Raggam <thetetet@gmail.com> +Repository: plone.api + + +Branch: refs/heads/master +Date: 2024-12-06T22:46:21-08:00 +Author: Steve Piercy (stevepiercy) +Commit: https://github.com/plone/plone.api/commit/5c7e0feaed0e5c8537953985e7aabe0b542b5efc + +Remove references to non-functional Coveralls + +Coveralls has not worked since Travis CI was removed back in 2021(?). Documentation still shows it is being used. It hasn't been missed. + +- Closes #537 +- See #233, which I think should be closed. + +Files changed: +M README.md +M docs/contribute.md + +b'diff --git a/README.md b/README.md\nindex 481eec7e..1bf7c5bc 100644\n--- a/README.md\n+++ b/README.md\n@@ -40,6 +40,3 @@ Issues\n Continuous Integration\n tested on [GitHub Actions](https://github.com/plone/plone.api/actions).\n \n-Code Coverage\n- is measured at [Coveralls.io](https://coveralls.io/github/plone/plone.api).\n-\ndiff --git a/docs/contribute.md b/docs/contribute.md\nindex 4df6de3a..ebacea56 100644\n--- a/docs/contribute.md\n+++ b/docs/contribute.md\n@@ -241,4 +241,3 @@ The documentation is rendered with a link from the API reference to the narrativ\n - {doc}`plone:index`\n - [Source code](https://github.com/plone/plone.api)\n - [Issue tracker](https://github.com/plone/plone.api/issues)\n-- [Code Coverage](https://coveralls.io/github/plone/plone.api)\n' + +Repository: plone.api + + +Branch: refs/heads/master +Date: 2024-12-06T22:48:12-08:00 +Author: Steve Piercy (stevepiercy) +Commit: https://github.com/plone/plone.api/commit/86ae02909232d9d366cb4e13db4515525fef487a + +news Files changed: -A news/4054.feature -M Products/CMFPlone/resources/browser/resource.py -M Products/CMFPlone/tests/testResourceRegistries.py +A news/543.documentation -b'diff --git a/Products/CMFPlone/resources/browser/resource.py b/Products/CMFPlone/resources/browser/resource.py\nindex aba363c73d..d3938fa805 100644\n--- a/Products/CMFPlone/resources/browser/resource.py\n+++ b/Products/CMFPlone/resources/browser/resource.py\n@@ -79,9 +79,15 @@ def update(self):\n registry_group_js = webresource.ResourceGroup(\n name="registry_js", group=root_group_js\n )\n+ registry_group_js_deferred = webresource.ResourceGroup(\n+ name="registry_js_deferred",\n+ )\n registry_group_css = webresource.ResourceGroup(\n name="registry_css", group=root_group_css\n )\n+ registry_group_css_deferred = webresource.ResourceGroup(\n+ name="registry_css_deferred",\n+ )\n records = self.registry.collectionOfInterface(\n IBundleRegistry, prefix="plone.bundles", check=False\n )\n@@ -107,7 +113,7 @@ def check_dependencies(bundle_name, depends, bundles):\n valid_dependencies = []\n \n for name in depend_names:\n- if name in bundles:\n+ if name in bundles or name == "all":\n valid_dependencies.append(name)\n continue\n if name in all_names:\n@@ -146,6 +152,11 @@ def check_dependencies(bundle_name, depends, bundles):\n if depends == "__broken__":\n continue\n external = self.is_external_url(record.jscompilation)\n+ r_group = registry_group_js\n+ if "all" in depends:\n+ # move to a separate group which is rendered after all others\n+ r_group = registry_group_js_deferred\n+ depends = None\n PloneScriptResource(\n context=self.context,\n name=name,\n@@ -155,7 +166,7 @@ def check_dependencies(bundle_name, depends, bundles):\n include=include,\n expression=record.expression,\n unique=unique,\n- group=registry_group_js,\n+ group=r_group,\n url=record.jscompilation if external else None,\n crossorigin="anonymous" if external else None,\n async_=record.load_async or None,\n@@ -168,6 +179,11 @@ def check_dependencies(bundle_name, depends, bundles):\n if depends == "__broken__":\n continue\n external = self.is_external_url(record.csscompilation)\n+ r_group = registry_group_css\n+ if "all" in depends:\n+ # move to a separate group which is rendered after all others\n+ r_group = registry_group_css_deferred\n+ depends = None\n PloneStyleResource(\n context=self.context,\n name=name,\n@@ -177,7 +193,7 @@ def check_dependencies(bundle_name, depends, bundles):\n include=include,\n expression=record.expression,\n unique=unique,\n- group=registry_group_css,\n+ group=r_group,\n url=record.csscompilation if external else None,\n media="all",\n rel="stylesheet",\n@@ -237,7 +253,11 @@ def check_dependencies(bundle_name, depends, bundles):\n **{"data-bundle": "diazo"},\n )\n \n- # add Custom CSS\n+ # add "deferred" groups at this point\n+ root_group_js.add(registry_group_js_deferred)\n+ root_group_css.add(registry_group_css_deferred)\n+\n+ # add Custom CSS always after everything else\n registry = getUtility(IRegistry)\n theme_settings = registry.forInterface(IThemeSettings, False)\n if theme_settings.custom_css:\ndiff --git a/Products/CMFPlone/tests/testResourceRegistries.py b/Products/CMFPlone/tests/testResourceRegistries.py\nindex f33fa292bc..c27639d5a8 100644\n--- a/Products/CMFPlone/tests/testResourceRegistries.py\n+++ b/Products/CMFPlone/tests/testResourceRegistries.py\n@@ -1,3 +1,4 @@\n+from lxml import etree\n from OFS.Image import File\n from plone.app.testing import logout\n from plone.app.testing import setRoles\n@@ -20,17 +21,17 @@\n \n \n class TestScriptsViewlet(PloneTestCase.PloneTestCase):\n- def _make_test_bundle(self):\n+ def _make_test_bundle(self, name="foobar", depends=""):\n registry = getUtility(IRegistry)\n \n bundles = registry.collectionOfInterface(\n IBundleRegistry, prefix="plone.bundles"\n )\n- bundle = bundles.add("foobar")\n- bundle.name = "foobar"\n- bundle.jscompilation = "http://foo.bar/foobar.js"\n- bundle.csscompilation = "http://foo.bar/foobar.css"\n- bundle.resources = ["foobar"]\n+ bundle = bundles.add(name)\n+ bundle.name = name\n+ bundle.jscompilation = f"http://foo.bar/{name}.js"\n+ bundle.csscompilation = f"http://foo.bar/{name}.css"\n+ bundle.depends = depends\n return bundle\n \n def test_bundle_defernot_asyncnot(self):\n@@ -173,6 +174,75 @@ def test_bundle_depends_on_multiple(self):\n results = view.render()\n self.assertIn("http://foo.bar/foobar.js", results)\n \n+ def test_js_bundle_depends_all(self):\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="a")\n+\n+ # Create a test bundle, which depends on "all" other and thus rendered\n+ # last.\n+ self._make_test_bundle(name="last", depends="all")\n+\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="b")\n+\n+ view = ScriptsView(self.layer["portal"], self.layer["request"], None)\n+ view.update()\n+ results = view.render()\n+\n+ parser = etree.HTMLParser()\n+ parsed = etree.fromstring(results, parser)\n+ scripts = parsed.xpath("//script")\n+\n+ # The last element is our JS, depending on "all".\n+ self.assertEqual(\n+ "http://foo.bar/last.js",\n+ scripts[-1].attrib["src"],\n+ )\n+\n+ # The first resource is our JS, which was defined with unspecified\n+ # dependency first.\n+ self.assertEqual(\n+ "http://foo.bar/a.js",\n+ scripts[0].attrib["src"],\n+ )\n+\n+ # The second resource is our JS, which was defined with unspecified\n+ # dependency last.\n+ self.assertEqual(\n+ "http://foo.bar/b.js",\n+ scripts[1].attrib["src"],\n+ )\n+\n+ # When more bundles depend on "all", they are ordered alphabetically\n+ # at the end.\n+ self._make_test_bundle(name="x-very-last", depends="all")\n+ self._make_test_bundle(name="a-last", depends="all")\n+\n+ # make sure cache purged\n+ setattr(self.layer["request"], REQUEST_CACHE_KEY, None)\n+\n+ view.update()\n+ results = view.render()\n+\n+ parsed = etree.fromstring(results, parser)\n+ scripts = parsed.xpath("//script")\n+\n+ # All the "all" depending bundles are sorted alphabetically at the end.\n+ self.assertEqual(\n+ "http://foo.bar/x-very-last.js",\n+ scripts[-1].attrib["src"],\n+ )\n+ self.assertEqual(\n+ "http://foo.bar/last.js",\n+ scripts[-2].attrib["src"],\n+ )\n+ self.assertEqual(\n+ "http://foo.bar/a-last.js",\n+ scripts[-3].attrib["src"],\n+ )\n+\n def test_bundle_depends_on_missing(self):\n bundle = self._make_test_bundle()\n bundle.depends = "nonexistsinbundle"\n@@ -217,6 +287,19 @@ def test_relative_uri_resource(self):\n \n \n class TestStylesViewlet(PloneTestCase.PloneTestCase):\n+ def _make_test_bundle(self, name="foobar", depends=""):\n+ registry = getUtility(IRegistry)\n+\n+ bundles = registry.collectionOfInterface(\n+ IBundleRegistry, prefix="plone.bundles"\n+ )\n+ bundle = bundles.add(name)\n+ bundle.name = name\n+ bundle.jscompilation = f"http://foo.bar/{name}.js"\n+ bundle.csscompilation = f"http://foo.bar/{name}.css"\n+ bundle.depends = depends\n+ return bundle\n+\n def test_styles_viewlet(self):\n styles = StylesView(self.layer["portal"], self.layer["request"], None)\n styles.update()\n@@ -323,6 +406,86 @@ def test_remove_bundle_on_request_with_subrequest(self):\n result = scripts.render()\n self.assertNotIn("http://test.foo/test.min.js", result)\n \n+ def test_css_bundle_depends_all(self):\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="a")\n+\n+ # Create a test bundle, which depends on "all" other and thus rendered\n+ # last.\n+ self._make_test_bundle(name="last", depends="all")\n+\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="b")\n+\n+ view = StylesView(self.layer["portal"], self.layer["request"], None)\n+ view.update()\n+ results = view.render()\n+\n+ parser = etree.HTMLParser()\n+ parsed = etree.fromstring(results, parser)\n+ styles = parsed.xpath("//link")\n+\n+ # The last element is our CSS, depending on "all".\n+ self.assertEqual(\n+ "http://foo.bar/last.css",\n+ styles[-1].attrib["href"],\n+ )\n+\n+ # The second last element is the theme barceloneta theme CSS.\n+ self.assertTrue(\n+ "++theme++barceloneta/css/barceloneta.min.css" in styles[-2].attrib["href"],\n+ )\n+\n+ # The first resource is our CSS, which was defined with unspecified\n+ # dependency.\n+ self.assertEqual(\n+ "http://foo.bar/a.css",\n+ styles[0].attrib["href"],\n+ )\n+\n+ # The second resource is our CSS, which was defined with unspecified\n+ # dependency first.\n+ self.assertEqual(\n+ "http://foo.bar/b.css",\n+ styles[1].attrib["href"],\n+ )\n+\n+ def test_css_bundle_depends_all_but_custom(self):\n+ registry = getUtility(IRegistry)\n+\n+ custom_key = "plone.app.theming.interfaces.IThemeSettings.custom_css"\n+ registry[custom_key] = "html { background-color: red; }"\n+\n+ # Create a test bundle, which depends on "all" other and thus rendered\n+ # after all except the custom styles.\n+ self._make_test_bundle(name="almost-last", depends="all")\n+\n+ view = StylesView(self.layer["portal"], self.layer["request"], None)\n+ view.update()\n+ results = view.render()\n+\n+ parser = etree.HTMLParser()\n+ parsed = etree.fromstring(results, parser)\n+ styles = parsed.xpath("//link")\n+\n+ # The last element is are the custom styles.\n+ self.assertTrue(\n+ "@@custom.css" in styles[-1].attrib["href"],\n+ )\n+\n+ # The second last element is now our CSS, depending on "all".\n+ self.assertEqual(\n+ "http://foo.bar/almost-last.css",\n+ styles[-2].attrib["href"],\n+ )\n+\n+ # The third last element is the theme barceloneta theme CSS.\n+ self.assertTrue(\n+ "++theme++barceloneta/css/barceloneta.min.css" in styles[-3].attrib["href"],\n+ )\n+\n \n class TestExpressions(PloneTestCase.PloneTestCase):\n def logout(self):\ndiff --git a/news/4054.feature b/news/4054.feature\nnew file mode 100644\nindex 0000000000..c7a171ee2a\n--- /dev/null\n+++ b/news/4054.feature\n@@ -0,0 +1,18 @@\n+Allow bundles to be rendered after all others.\n+\n+JS and CSS resources can now be rendered after all other resources in their\n+resource group including the theme (e.g. the Barceloneta theme CSS).\n+\n+There is an exception for custom CSS which can be defined in the theming\n+controlpanel. This one is always rendered as last style resource.\n+\n+To render resources after all others, give them the "depends" value of "all".\n+For each of these resources, "all" indicates that the resource depends on all other resources, making it render after its dependencies.\n+If you set multiple resources with "all", then they will render alphabetically after all other.\n+\n+This lets you override a theme with custom CSS from a bundle instead of having\n+to add the CSS customizations to the registry via the "custom_css" settings.\n+As a consequence, theme customization can now be done in the filesystem in\n+ordinary CSS files instead of being bound to a time consuming workflow which\n+involves upgrading the custom_css registry after every change.\n+[thet, petschki]\n' +b'diff --git a/news/543.documentation b/news/543.documentation\nnew file mode 100644\nindex 00000000..74efef8b\n--- /dev/null\n+++ b/news/543.documentation\n@@ -0,0 +1 @@\n+Remove references to unused Coveralls. @stevepiercy\n' -Repository: Products.CMFPlone +Repository: plone.api -Branch: refs/heads/6.0.x -Date: 2024-12-06T08:13:28+01:00 -Author: Peter Mathis (petschki) -Commit: https://github.com/plone/Products.CMFPlone/commit/33b278811031fb154501226d408886ac187359fb +Branch: refs/heads/master +Date: 2024-12-09T13:59:52-08:00 +Author: Steve Piercy (stevepiercy) +Commit: https://github.com/plone/plone.api/commit/a6145e06a8a6d4c7fd68f0b87ddea68213d4a979 -Merge pull request #4077 from plone/petschki-deferred-resourcegroups-60 +Merge pull request #543 from plone/remove-unused-coveralls -Implement resource bundle depends on `all` to be rendered at last (6.0 branch) +Remove references to non-functional Coveralls Files changed: -A news/4054.feature -M Products/CMFPlone/resources/browser/resource.py -M Products/CMFPlone/tests/testResourceRegistries.py +A news/543.documentation +M README.md +M docs/contribute.md -b'diff --git a/Products/CMFPlone/resources/browser/resource.py b/Products/CMFPlone/resources/browser/resource.py\nindex aba363c73d..d3938fa805 100644\n--- a/Products/CMFPlone/resources/browser/resource.py\n+++ b/Products/CMFPlone/resources/browser/resource.py\n@@ -79,9 +79,15 @@ def update(self):\n registry_group_js = webresource.ResourceGroup(\n name="registry_js", group=root_group_js\n )\n+ registry_group_js_deferred = webresource.ResourceGroup(\n+ name="registry_js_deferred",\n+ )\n registry_group_css = webresource.ResourceGroup(\n name="registry_css", group=root_group_css\n )\n+ registry_group_css_deferred = webresource.ResourceGroup(\n+ name="registry_css_deferred",\n+ )\n records = self.registry.collectionOfInterface(\n IBundleRegistry, prefix="plone.bundles", check=False\n )\n@@ -107,7 +113,7 @@ def check_dependencies(bundle_name, depends, bundles):\n valid_dependencies = []\n \n for name in depend_names:\n- if name in bundles:\n+ if name in bundles or name == "all":\n valid_dependencies.append(name)\n continue\n if name in all_names:\n@@ -146,6 +152,11 @@ def check_dependencies(bundle_name, depends, bundles):\n if depends == "__broken__":\n continue\n external = self.is_external_url(record.jscompilation)\n+ r_group = registry_group_js\n+ if "all" in depends:\n+ # move to a separate group which is rendered after all others\n+ r_group = registry_group_js_deferred\n+ depends = None\n PloneScriptResource(\n context=self.context,\n name=name,\n@@ -155,7 +166,7 @@ def check_dependencies(bundle_name, depends, bundles):\n include=include,\n expression=record.expression,\n unique=unique,\n- group=registry_group_js,\n+ group=r_group,\n url=record.jscompilation if external else None,\n crossorigin="anonymous" if external else None,\n async_=record.load_async or None,\n@@ -168,6 +179,11 @@ def check_dependencies(bundle_name, depends, bundles):\n if depends == "__broken__":\n continue\n external = self.is_external_url(record.csscompilation)\n+ r_group = registry_group_css\n+ if "all" in depends:\n+ # move to a separate group which is rendered after all others\n+ r_group = registry_group_css_deferred\n+ depends = None\n PloneStyleResource(\n context=self.context,\n name=name,\n@@ -177,7 +193,7 @@ def check_dependencies(bundle_name, depends, bundles):\n include=include,\n expression=record.expression,\n unique=unique,\n- group=registry_group_css,\n+ group=r_group,\n url=record.csscompilation if external else None,\n media="all",\n rel="stylesheet",\n@@ -237,7 +253,11 @@ def check_dependencies(bundle_name, depends, bundles):\n **{"data-bundle": "diazo"},\n )\n \n- # add Custom CSS\n+ # add "deferred" groups at this point\n+ root_group_js.add(registry_group_js_deferred)\n+ root_group_css.add(registry_group_css_deferred)\n+\n+ # add Custom CSS always after everything else\n registry = getUtility(IRegistry)\n theme_settings = registry.forInterface(IThemeSettings, False)\n if theme_settings.custom_css:\ndiff --git a/Products/CMFPlone/tests/testResourceRegistries.py b/Products/CMFPlone/tests/testResourceRegistries.py\nindex f33fa292bc..c27639d5a8 100644\n--- a/Products/CMFPlone/tests/testResourceRegistries.py\n+++ b/Products/CMFPlone/tests/testResourceRegistries.py\n@@ -1,3 +1,4 @@\n+from lxml import etree\n from OFS.Image import File\n from plone.app.testing import logout\n from plone.app.testing import setRoles\n@@ -20,17 +21,17 @@\n \n \n class TestScriptsViewlet(PloneTestCase.PloneTestCase):\n- def _make_test_bundle(self):\n+ def _make_test_bundle(self, name="foobar", depends=""):\n registry = getUtility(IRegistry)\n \n bundles = registry.collectionOfInterface(\n IBundleRegistry, prefix="plone.bundles"\n )\n- bundle = bundles.add("foobar")\n- bundle.name = "foobar"\n- bundle.jscompilation = "http://foo.bar/foobar.js"\n- bundle.csscompilation = "http://foo.bar/foobar.css"\n- bundle.resources = ["foobar"]\n+ bundle = bundles.add(name)\n+ bundle.name = name\n+ bundle.jscompilation = f"http://foo.bar/{name}.js"\n+ bundle.csscompilation = f"http://foo.bar/{name}.css"\n+ bundle.depends = depends\n return bundle\n \n def test_bundle_defernot_asyncnot(self):\n@@ -173,6 +174,75 @@ def test_bundle_depends_on_multiple(self):\n results = view.render()\n self.assertIn("http://foo.bar/foobar.js", results)\n \n+ def test_js_bundle_depends_all(self):\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="a")\n+\n+ # Create a test bundle, which depends on "all" other and thus rendered\n+ # last.\n+ self._make_test_bundle(name="last", depends="all")\n+\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="b")\n+\n+ view = ScriptsView(self.layer["portal"], self.layer["request"], None)\n+ view.update()\n+ results = view.render()\n+\n+ parser = etree.HTMLParser()\n+ parsed = etree.fromstring(results, parser)\n+ scripts = parsed.xpath("//script")\n+\n+ # The last element is our JS, depending on "all".\n+ self.assertEqual(\n+ "http://foo.bar/last.js",\n+ scripts[-1].attrib["src"],\n+ )\n+\n+ # The first resource is our JS, which was defined with unspecified\n+ # dependency first.\n+ self.assertEqual(\n+ "http://foo.bar/a.js",\n+ scripts[0].attrib["src"],\n+ )\n+\n+ # The second resource is our JS, which was defined with unspecified\n+ # dependency last.\n+ self.assertEqual(\n+ "http://foo.bar/b.js",\n+ scripts[1].attrib["src"],\n+ )\n+\n+ # When more bundles depend on "all", they are ordered alphabetically\n+ # at the end.\n+ self._make_test_bundle(name="x-very-last", depends="all")\n+ self._make_test_bundle(name="a-last", depends="all")\n+\n+ # make sure cache purged\n+ setattr(self.layer["request"], REQUEST_CACHE_KEY, None)\n+\n+ view.update()\n+ results = view.render()\n+\n+ parsed = etree.fromstring(results, parser)\n+ scripts = parsed.xpath("//script")\n+\n+ # All the "all" depending bundles are sorted alphabetically at the end.\n+ self.assertEqual(\n+ "http://foo.bar/x-very-last.js",\n+ scripts[-1].attrib["src"],\n+ )\n+ self.assertEqual(\n+ "http://foo.bar/last.js",\n+ scripts[-2].attrib["src"],\n+ )\n+ self.assertEqual(\n+ "http://foo.bar/a-last.js",\n+ scripts[-3].attrib["src"],\n+ )\n+\n def test_bundle_depends_on_missing(self):\n bundle = self._make_test_bundle()\n bundle.depends = "nonexistsinbundle"\n@@ -217,6 +287,19 @@ def test_relative_uri_resource(self):\n \n \n class TestStylesViewlet(PloneTestCase.PloneTestCase):\n+ def _make_test_bundle(self, name="foobar", depends=""):\n+ registry = getUtility(IRegistry)\n+\n+ bundles = registry.collectionOfInterface(\n+ IBundleRegistry, prefix="plone.bundles"\n+ )\n+ bundle = bundles.add(name)\n+ bundle.name = name\n+ bundle.jscompilation = f"http://foo.bar/{name}.js"\n+ bundle.csscompilation = f"http://foo.bar/{name}.css"\n+ bundle.depends = depends\n+ return bundle\n+\n def test_styles_viewlet(self):\n styles = StylesView(self.layer["portal"], self.layer["request"], None)\n styles.update()\n@@ -323,6 +406,86 @@ def test_remove_bundle_on_request_with_subrequest(self):\n result = scripts.render()\n self.assertNotIn("http://test.foo/test.min.js", result)\n \n+ def test_css_bundle_depends_all(self):\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="a")\n+\n+ # Create a test bundle, which depends on "all" other and thus rendered\n+ # last.\n+ self._make_test_bundle(name="last", depends="all")\n+\n+ # Create a test bundle, which has unspecified dependencies and is\n+ # rendered in order as defined.\n+ self._make_test_bundle(name="b")\n+\n+ view = StylesView(self.layer["portal"], self.layer["request"], None)\n+ view.update()\n+ results = view.render()\n+\n+ parser = etree.HTMLParser()\n+ parsed = etree.fromstring(results, parser)\n+ styles = parsed.xpath("//link")\n+\n+ # The last element is our CSS, depending on "all".\n+ self.assertEqual(\n+ "http://foo.bar/last.css",\n+ styles[-1].attrib["href"],\n+ )\n+\n+ # The second last element is the theme barceloneta theme CSS.\n+ self.assertTrue(\n+ "++theme++barceloneta/css/barceloneta.min.css" in styles[-2].attrib["href"],\n+ )\n+\n+ # The first resource is our CSS, which was defined with unspecified\n+ # dependency.\n+ self.assertEqual(\n+ "http://foo.bar/a.css",\n+ styles[0].attrib["href"],\n+ )\n+\n+ # The second resource is our CSS, which was defined with unspecified\n+ # dependency first.\n+ self.assertEqual(\n+ "http://foo.bar/b.css",\n+ styles[1].attrib["href"],\n+ )\n+\n+ def test_css_bundle_depends_all_but_custom(self):\n+ registry = getUtility(IRegistry)\n+\n+ custom_key = "plone.app.theming.interfaces.IThemeSettings.custom_css"\n+ registry[custom_key] = "html { background-color: red; }"\n+\n+ # Create a test bundle, which depends on "all" other and thus rendered\n+ # after all except the custom styles.\n+ self._make_test_bundle(name="almost-last", depends="all")\n+\n+ view = StylesView(self.layer["portal"], self.layer["request"], None)\n+ view.update()\n+ results = view.render()\n+\n+ parser = etree.HTMLParser()\n+ parsed = etree.fromstring(results, parser)\n+ styles = parsed.xpath("//link")\n+\n+ # The last element is are the custom styles.\n+ self.assertTrue(\n+ "@@custom.css" in styles[-1].attrib["href"],\n+ )\n+\n+ # The second last element is now our CSS, depending on "all".\n+ self.assertEqual(\n+ "http://foo.bar/almost-last.css",\n+ styles[-2].attrib["href"],\n+ )\n+\n+ # The third last element is the theme barceloneta theme CSS.\n+ self.assertTrue(\n+ "++theme++barceloneta/css/barceloneta.min.css" in styles[-3].attrib["href"],\n+ )\n+\n \n class TestExpressions(PloneTestCase.PloneTestCase):\n def logout(self):\ndiff --git a/news/4054.feature b/news/4054.feature\nnew file mode 100644\nindex 0000000000..c7a171ee2a\n--- /dev/null\n+++ b/news/4054.feature\n@@ -0,0 +1,18 @@\n+Allow bundles to be rendered after all others.\n+\n+JS and CSS resources can now be rendered after all other resources in their\n+resource group including the theme (e.g. the Barceloneta theme CSS).\n+\n+There is an exception for custom CSS which can be defined in the theming\n+controlpanel. This one is always rendered as last style resource.\n+\n+To render resources after all others, give them the "depends" value of "all".\n+For each of these resources, "all" indicates that the resource depends on all other resources, making it render after its dependencies.\n+If you set multiple resources with "all", then they will render alphabetically after all other.\n+\n+This lets you override a theme with custom CSS from a bundle instead of having\n+to add the CSS customizations to the registry via the "custom_css" settings.\n+As a consequence, theme customization can now be done in the filesystem in\n+ordinary CSS files instead of being bound to a time consuming workflow which\n+involves upgrading the custom_css registry after every change.\n+[thet, petschki]\n' +b'diff --git a/README.md b/README.md\nindex 481eec7e..1bf7c5bc 100644\n--- a/README.md\n+++ b/README.md\n@@ -40,6 +40,3 @@ Issues\n Continuous Integration\n tested on [GitHub Actions](https://github.com/plone/plone.api/actions).\n \n-Code Coverage\n- is measured at [Coveralls.io](https://coveralls.io/github/plone/plone.api).\n-\ndiff --git a/docs/contribute.md b/docs/contribute.md\nindex 4df6de3a..ebacea56 100644\n--- a/docs/contribute.md\n+++ b/docs/contribute.md\n@@ -241,4 +241,3 @@ The documentation is rendered with a link from the API reference to the narrativ\n - {doc}`plone:index`\n - [Source code](https://github.com/plone/plone.api)\n - [Issue tracker](https://github.com/plone/plone.api/issues)\n-- [Code Coverage](https://coveralls.io/github/plone/plone.api)\ndiff --git a/news/543.documentation b/news/543.documentation\nnew file mode 100644\nindex 00000000..74efef8b\n--- /dev/null\n+++ b/news/543.documentation\n@@ -0,0 +1 @@\n+Remove references to unused Coveralls. @stevepiercy\n'