From dcf79662a6a0c39336e9272d84b72be8b2c478ec Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Fri, 20 Sep 2024 16:28:37 -0700 Subject: [PATCH 01/26] Switch to Plone Sphinx Theme for documentation, include changes to http-examples to build docs preview (#1815) * Include changes to http-examples to build docs preview * Remove obsolete styles and template * Update requirements-docs.txt and conf.py to align with minimal Plone Sphinx Theme * Update requirements-docs.txt and conf.py to align with minimal Plone Sphinx Theme * Only install docs requirements when building docs * Add setuptools to requirements-docs.txt because sphinxcontrib.httpexample requires its pkg_resources * improve comment for future developers * news * Remove commented code * Move legacy commented setting descriptions above their settings * Move python-clean into a separate make target, so docs-clean does not obliterate the virtual environment. Most of the time, I just want to purge the built docs, not the virtual environment. --- .github/workflows/tests.yml | 4 +- .readthedocs.yaml | 2 +- Makefile | 7 +- docs/source/_static/custom.css | 353 ------------------------------- docs/source/_static/print.css | 3 - docs/source/_templates/page.html | 16 -- docs/source/conf.py | 95 ++++++--- news/1815.documentation | 1 + requirements-docs.txt | 16 +- 9 files changed, 79 insertions(+), 418 deletions(-) delete mode 100644 docs/source/_static/custom.css delete mode 100644 docs/source/_static/print.css delete mode 100644 docs/source/_templates/page.html create mode 100644 news/1815.documentation diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b354a9e4d5..64f7dc2ee6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -50,7 +50,7 @@ jobs: - run: pip install virtualenv - run: pip install wheel - name: pip install - run: pip install -r requirements-${{ matrix.plone-version }}.txt -r requirements-docs.txt + run: pip install -r requirements-${{ matrix.plone-version }}.txt # buildout - name: buildout @@ -60,7 +60,7 @@ jobs: # build sphinx - name: sphinx - run: if [ "${{ matrix.plone-version }}" == "6.0" ] && [ ${{ matrix.python-version }} == '3.12' ]; then make docs-html; fi + run: if [ "${{ matrix.plone-version }}" == "6.0" ] && [ ${{ matrix.python-version }} == '3.12' ]; then pip install -r requirements-docs.txt && make docs-html; fi # test - name: test diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0bf1b9ba85..043663c1c4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -18,7 +18,7 @@ build: # If there are no changes (git diff exits with 0) we force the command to return with 183. # This is a special exit code on Read the Docs that will cancel the build immediately. - | - if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ .readthedocs.yaml requirements-docs.txt requirements.txt; + if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ src/plone/restapi/tests/http-examples/ .readthedocs.yaml requirements-docs.txt requirements.txt; then exit 183; fi diff --git a/Makefile b/Makefile index 71c500930e..2e931a857b 100644 --- a/Makefile +++ b/Makefile @@ -95,10 +95,13 @@ black: ## Black zpretty: ## zpretty if [ -f "bin/zpretty" ]; then zpretty -i ./**/*.zcml; fi +.PHONY: python-clean +python-clean: ## Clean Python virtual environment + rm -rf bin include lib + .PHONY: docs-clean -docs-clean: ## Clean current and legacy docs build directories, and Python virtual environment +docs-clean: ## Clean current and legacy docs build directories cd $(DOCS_DIR) && rm -rf $(BUILDDIR)/ - rm -rf bin include lib rm -rf docs/build .PHONY: docs-html diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css deleted file mode 100644 index 09d09e359b..0000000000 --- a/docs/source/_static/custom.css +++ /dev/null @@ -1,353 +0,0 @@ -:root { - /* Add Font Awesome 5 icon and color for `todo` */ - --pst-icon-clipboard-list: '\f46d'; - --pst-icon-admonition-todo: var(--pst-icon-clipboard-list); - --pst-color-admonition-todo: 161 , 46, 233; - --target-color: #b9ee9e; - --codeblock-color: #aad993; -} - -.visuallyhidden { - display: none; -} -pre { - border-radius: 0; - background-color: white; - box-shadow: none; -} -a, -a:visited, -main.bd-content #main-content a, -main.bd-content #main-content a:visited { - color: #2980b9; -} -a:hover, -main.bd-content #main-content a:hover { - color: #1a567e; - text-decoration: none; -} -ul { - list-style-type: square; -} -ul li > p { - margin-bottom: 0.3rem; -} -ol li > p { - margin-bottom: 0.3rem; -} -img{ - margin: 1rem 0; -} -figure img, -.figure img { - box-shadow: 0 6px 24px 0 rgba(153,153,153,0.3); -} -.sidebar img.logo { - box-shadow: none; - width: 200px; - margin-bottom: 1rem; -} -span.linenos { - padding-right: 1em; -} -p.ploneorglink img { - vertical-align: bottom; -} -dt:target, -span.highlighted, -ul.search li span.highlighted { - background-color: var(--target-color); -} - -.bd-sidebar .nav ul { - padding: 0 0 0 1rem; -} -.bd-sidebar .nav .toctree-checkbox ~ label i { - transform: rotate(90deg); -} -.bd-sidebar .nav .toctree-checkbox:checked ~ label i { - transform: rotate(0deg); -} - -.toctree-wrapper .caption { - font-weight: bold; - font-size: 1.2em; - margin-top: 3rem; -} -.toctree-wrapper ul { - list-style: none; -} - -section:not(#glossary) h1 ~ dl { - display: grid; - grid-template-columns: max-content auto; -} -section:not(#glossary) h1 ~ dl dd { - margin-bottom: unset !important; -} - -div.section { - margin-bottom: 5rem; -} - -/* admonitions */ -.admonition { - border-radius: 0; - border: none; - border-left: .2rem solid; - border-left-color: rgba(var(--pst-color-admonition-default),1); -} -.admonition .admonition-title { - margin-bottom: 1.5rem !important; -} -.admonition.toggle .admonition-title { - cursor: pointer; - display: flex; -} -.admonition.toggle .admonition-title::after { - content: "\f105"; - font-weight: 900; - font-family: "Font Awesome 5 Free"; - margin-left: auto; -} -.admonition.toggle .admonition-title.open::after { - content: "\f107"; -} -/* admonition `todo` */ -.admonition.admonition-todo, -div.admonition.admonition-todo { - border-color: rgba(var(--pst-color-admonition-todo),1); -} -.admonition.admonition-todo > .admonition-title, -div.admonition.admonition-todo > .admonition-title { - background-color: rgba(var(--pst-color-admonition-todo),.1); -} -.admonition.admonition-todo > .admonition-title::before, -div.admonition.admonition-todo > .admonition-title::before { - color: rgba(var(--pst-color-admonition-todo),1); - content: var(--pst-icon-admonition-todo); -} -.admonition-github-only.admonition { - display: none; -} - - -.topic { - padding: 1.5em 1em .5em 1em; -} -.topic-title { - font-weight: bold; -} - - -/* Bootstrap */ -.btn-primary { - color: #fff; - background-color: #2980b9; - border-color: #2980b9; -} -.btn-primary { - background-color: #1f86ca; - border-color: #2980b9; -} - -/* Search */ - -/* Show search form. It is hidden by default. */ -#search-documentation, -#search-documentation~form, -#search-documentation~p { - display:block; -} -ul.search { - margin-left: 0; -} -p.search-summary { - margin: 1em 0 2rem 0; -} -#search-results ul { - list-style-type: none; - padding-left: 0; -} -#search-results ul li, -ul.search li { - margin-bottom: 2rem; - padding: 0; - background-image: none; - border-bottom: none; -} -#search-results ul li h3 { - margin: 0.4rem 0 .5rem; - font-size: 1.5rem; -} -#search-results ul li .breadcrumbs { - display: flex; - flex-direction: row; - flex-wrap: wrap; -} -#search-results ul li .breadcrumbs a { - font-weight: normal; -} -#search-results ul li .breadcrumbs .lastbreadcrumb { - white-space: nowrap; - display: inline-block; - max-width: 12rem; - overflow: hidden; /* "overflow"-Wert darf nicht "visible" sein */ - - text-overflow: ellipsis; -} -ul.search li p.context { - margin-left: 0; -} -.pathseparator { - padding: 0 0.7rem; -} - - -/* submenu */ -.bd-toc { - box-shadow: 0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1); -} - -/* extra sidebar */ -div.sidebar:not(.margin){ - width: 40%; - float: right; - clear: right; - margin: .3rem 0 .3rem 0.5em; - padding: 2rem 0 1.5rem 1rem !important; - background-color: rgba(var(--pst-color-admonition-note),.1); - border: none; - border-left: 8px rgba(var(--pst-color-admonition-default),1) solid; - border-radius: .2rem; - box-shadow: 0 .2rem .5rem rgba(0,0,0,.05),0 0 .0625rem rgba(0,0,0,.1); -} - -div.sidebar:not(.margin) .figure { - margin-top: 0; - padding-top: 0; - margin-left: 0; - padding-left: 0; -} -div.sidebar:not(.margin) img.logo { - margin-top: 0; - margin-bottom: .3rem; -} -div.sidebar:not(.margin) p { - margin-bottom: 0; -} -div.sidebar:not(.margin) p.sidebar-title { - display: none; -} -div.sidebar:not(.margin) div.topic { - padding: .5em 0; - background-color: transparent; - border: none; -} -div.sidebar:not(.margin) pre { - margin: .5em 0 1.5em 0; -} -div.sidebar:not(.margin) div[class*="highlight-"] { - margin-right: .5em; -} -div.sidebar:not(.margin) .admonition { - margin-right: .5em; - background-color: #ffffff; -} -@media (min-width:768px) { - div.sidebar:not(.margin) { - width: 50%; - margin-left: 1.5em; - margin-right: -28%; - } -} - - -main.bd-content #main-content dl.simple dt { - margin-top: .8em; -} -main.bd-content #main-content dl.simple dt:nth-of-type(1) { - margin-top: 0; -} -main.bd-content #main-content dl.simple dd { - margin-top: .8em; -} -main.bd-content #main-content dl.simple dt + dd { - margin-top: 0; -} - -.prev-next-bottom { - margin: 20px 0 30px 0; -} -.prev-next-bottom a.left-prev, .prev-next-bottom a.right-next { - padding: 5px 10px; - border: 1px solid rgba(0,0,0,.2); - max-width: 45%; - overflow-x: hidden; - color: rgba(0,0,0,.65); - border-radius: 10px; -} - -/* Local navigation */ -li.nav-item.toc-entry { - line-height: 1.25em; - margin-bottom: 0.25em; -} - -span.guilabel, span.menuselection { - border: none; - background: #e7f2fa; - border-radius: 4px; - padding: 4px 5px; - font-size: 90%; - font-weight: bold; - font-style: italic; - white-space: nowrap; -} - - -/* - * extensions - */ - -/* definitions */ -dl.py.function { - margin-bottom: 5rem; -} -dl.py.function > dt { - background-color: var(--codeblock-color); - padding: 4px 5px; -} -dl.py.function > dt:target { - background-color: var(--target-color); -} -dl.field-list > dt { - padding-left: 0; -} - -/* code blocks */ -div.viewcode-block:target { - padding: 10px 10px; - background-color: var(--codeblock-color); - border-top: 1px solid var(--codeblock-color); - border-bottom: 1px solid var(--codeblock-color); -} - -/* Hint for providing the Unicode Value of an fa-icon in CSS as ASCII - replace the "&#x" part from the unicode HTML5 literal - displayed on the font-awesome website cheatsheet e.g. - http://fontawesome.io/cheatsheet/ - - fa-wrench [] - - by a backslash like: - - .fa-wrench:before { - content: "\f0ad"; - } -*/ - -.icon-wrench:before, -.fa-wrench:before { - content: "\f0ad"; -} diff --git a/docs/source/_static/print.css b/docs/source/_static/print.css deleted file mode 100644 index 8dbc2d5794..0000000000 --- a/docs/source/_static/print.css +++ /dev/null @@ -1,3 +0,0 @@ -.tooltip { - display: none; -} diff --git a/docs/source/_templates/page.html b/docs/source/_templates/page.html deleted file mode 100644 index 35e79d84c1..0000000000 --- a/docs/source/_templates/page.html +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "!page.html" %} - -{% set css_files = css_files + ["_static/custom.css"] %} - -{% block footer %} - -{% endblock %} \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index c1cb7ad6f7..5bc2f084a2 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,6 +26,8 @@ # General information about the project. project = "plone.restapi" +author = "Plone Community" +trademark_name = "Plone" thisyear = datetime.datetime.now().year copyright = "2014-%s, Plone Foundation" % thisyear @@ -159,50 +161,85 @@ def patch_pygments_to_highlight_jsonschema(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = "sphinx_book_theme" +html_theme = "plone_sphinx_theme" +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. html_logo = "_static/logo.svg" -html_favicon = "_static/favicon.ico" -html_css_files = ["custom.css", ("print.css", {"media": "print"})] +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = "_static/favicon.ico" # See http://sphinx-doc.org/ext/todo.html#confval-todo_include_todos todo_include_todos = True +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +html_title = "%(project)s v%(release)s" % {"project": project, "release": release} + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. html_theme_options = { + "extra_footer": """

The text and illustrations in this website are licensed by the Plone Foundation under a Creative Commons Attribution 4.0 International license. Plone and the Plone® logo are registered trademarks of the Plone Foundation, registered in the United States and other countries. For guidelines on the permitted uses of the Plone trademarks, see https://plone.org/foundation/logo. All other trademarks are owned by their respective owners.

+

Pull request previews by Read the Docs.

""", + "footer_end": ["version.html"], + "icon_links": [ + { + "name": "GitHub", + "url": "https://github.com/plone/plone.restapi", + "icon": "fa-brands fa-square-github", + "type": "fontawesome", + "attributes": { + "target": "_blank", + "rel": "noopener me", + "class": "nav-link custom-fancy-css" + } + }, + { + "name": "Mastodon", + "url": "https://plone.social/@plone", + "icon": "fa-brands fa-mastodon", + "type": "fontawesome", + "attributes": { + "target": "_blank", + "rel": "noopener me", + "class": "nav-link custom-fancy-css" + } + }, + { + "name": "Twitter", + "url": "https://twitter.com/plone", + "icon": "fa-brands fa-square-twitter", + "type": "fontawesome", + "attributes": { + "target": "_blank", + "rel": "noopener me", + "class": "nav-link custom-fancy-css" + } + }, + ], + "logo": { + "text": html_title, + }, + "navigation_with_keys": True, "path_to_docs": "docs", - "repository_url": "https://github.com/plone/plone.restapi", "repository_branch": "master", - "use_repository_button": True, - "use_issues_button": True, + "repository_url": "https://github.com/plone/plone.restapi", + "search_bar_text": "Search", # TODO: Confirm usage of search_bar_text "use_edit_page_button": True, - "extra_footer": """

The text and illustrations in this website are licensed by the Plone Foundation under a Creative Commons Attribution 4.0 International license. Plone and the Plone® logo are registered trademarks of the Plone Foundation, registered in the United States and other countries. For guidelines on the permitted uses of the Plone trademarks, see https://plone.org/foundation/logo. All other trademarks are owned by their respective owners.

""", + "use_issues_button": True, + "use_repository_button": True, } -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = "%(project)s v%(release)s" % {"project": project, "release": release} - # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". @@ -274,12 +311,14 @@ def patch_pygments_to_highlight_jsonschema(): # For more information see: # https://myst-parser.readthedocs.io/en/latest/syntax/optional.html myst_enable_extensions = [ - "deflist", # You will be able to utilise definition lists + "deflist", # Support definition lists. # https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#definition-lists - "linkify", # Identify “bare” web URLs and add hyperlinks. + "linkify", # Identify "bare" web URLs and add hyperlinks. "colon_fence", # You can also use ::: delimiters to denote code fences,\ # instead of ```. - "substitution", # https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#substitutions-with-jinja2 + "substitution", # plone.restapi \ + # https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#substitutions-with-jinja2 + "html_image", # For inline images. See https://myst-parser.readthedocs.io/en/latest/syntax/optional.html#html-images ] myst_substitutions = { diff --git a/news/1815.documentation b/news/1815.documentation new file mode 100644 index 0000000000..dd9891dfe5 --- /dev/null +++ b/news/1815.documentation @@ -0,0 +1 @@ +Use Plone Sphinx Theme for documentation. Build docs when there are changes to http-examples. @stevepiercy diff --git a/requirements-docs.txt b/requirements-docs.txt index a522f65309..4b3918f397 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,20 +1,10 @@ -docutils<0.17,>=0.15 # sphinx-book-theme 0.2.0 has requirement docutils<0.17,>=0.15 -Sphinx<5,>=3 # sphinx-book-theme 0.3.3 has requirement sphinx<5,>=3 -sphinxcontrib-applehelp==1.0.4 # newer versions require sphinx 5 -sphinxcontrib-devhelp==1.0.2 # newer versions require sphinx 5 -sphinxcontrib-htmlhelp==2.0.1 # newer versions require sphinx 5 -sphinxcontrib-qthelp==1.0.3 # newer versions require sphinx 5 -sphinxcontrib-serializinghtml==1.1.5 # newer versions require sphinx 5 -jsx-lexer -lesscpy linkify-it-py myst-parser +plone-sphinx-theme +setuptools # required by sphinxcontrib.httpexample, remove when it migrates to importlib sphinx-autobuild -sphinx-book-theme sphinx-copybutton -sphinx-sitemap -sphinx-togglebutton sphinxcontrib.httpdomain sphinxcontrib.httpexample -sphinxcontrib-spelling sphinxext-opengraph +vale==2.30.0 From 913f49b7e48f6e7dc3dbea5ea72428611d16e796 Mon Sep 17 00:00:00 2001 From: Mauro Amico Date: Sat, 21 Sep 2024 02:06:15 +0200 Subject: [PATCH 02/26] fix show_excluded_items in @navigation api (#1816) * fix show_excluded_items in @navigation api * changelog * black * revert pyenv * typo Co-authored-by: Steve Piercy * Update 1816.bugfix Co-authored-by: Steve Piercy * fix tests --------- Co-authored-by: Steve Piercy --- news/1816.bugfix | 2 + src/plone/restapi/services/navigation/get.py | 4 +- .../restapi/tests/test_services_navigation.py | 37 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 news/1816.bugfix diff --git a/news/1816.bugfix b/news/1816.bugfix new file mode 100644 index 0000000000..2bb52fc581 --- /dev/null +++ b/news/1816.bugfix @@ -0,0 +1,2 @@ +Fix incorrect condition for ``show_excluded_items`` setting in the ``@navigation`` API. +[mamico] diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py index 665f22da01..a21a52abe4 100644 --- a/src/plone/restapi/services/navigation/get.py +++ b/src/plone/restapi/services/navigation/get.py @@ -135,7 +135,9 @@ def navtree(self): if brain_parent_path == navtree_path: # This should be already provided by the portal_tabs_view continue - if brain.exclude_from_nav and not context_path.startswith(brain_path): + if brain.exclude_from_nav and not f"{brain_path}/".startswith( + f"{context_path}/" + ): # skip excluded items if they're not in our context path continue url = brain.getURL() diff --git a/src/plone/restapi/tests/test_services_navigation.py b/src/plone/restapi/tests/test_services_navigation.py index dc466bc38e..890c5c1220 100644 --- a/src/plone/restapi/tests/test_services_navigation.py +++ b/src/plone/restapi/tests/test_services_navigation.py @@ -124,6 +124,43 @@ def test_dont_broke_with_contents_without_review_state(self): ) self.assertIsNone(response.json()["items"][1]["items"][3]["review_state"]) + def test_show_excluded_items(self): + registry = getUtility(IRegistry) + settings = registry.forInterface(INavigationSchema, prefix="plone") + + # Plone 5.2 and Plone 6.0 have different default values: + # False for Plone 6.0 and True for Plone 5.2 + # explicitly set the value to False to avoid test failures + settings.show_excluded_items = False + createContentInContainer( + self.folder, + "Folder", + id="excluded-subfolder", + title="Excluded SubFolder", + exclude_from_nav=True, + ) + transaction.commit() + response = self.api_session.get( + "/folder/@navigation", params={"expand.navigation.depth": 2} + ) + self.assertNotIn( + "Excluded SubFolder", + [item["title"] for item in response.json()["items"][1]["items"]], + ) + + # change setting to show excluded items + registry = getUtility(IRegistry) + settings = registry.forInterface(INavigationSchema, prefix="plone") + settings.show_excluded_items = True + transaction.commit() + response = self.api_session.get( + "/folder/@navigation", params={"expand.navigation.depth": 2} + ) + self.assertIn( + "Excluded SubFolder", + [item["title"] for item in response.json()["items"][1]["items"]], + ) + def test_navigation_sorting(self): registry = getUtility(IRegistry) registry["plone.displayed_types"] = ( From 682567a86b9fc022fbad32e27906475c5410eee1 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 21 Sep 2024 09:08:30 -0700 Subject: [PATCH 03/26] =?UTF-8?q?Revert=20duplication=20of=20pip=20install?= =?UTF-8?q?=20-r=20requirements-docs.txt=20in=20GitHub=20=E2=80=A6=20(#181?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert duplication of pip install -r requirements-docs.txt in GitHub workflow * news * Delete news/1817.internal --------- Co-authored-by: David Glick --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 64f7dc2ee6..73447527bc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,7 +60,7 @@ jobs: # build sphinx - name: sphinx - run: if [ "${{ matrix.plone-version }}" == "6.0" ] && [ ${{ matrix.python-version }} == '3.12' ]; then pip install -r requirements-docs.txt && make docs-html; fi + run: if [ "${{ matrix.plone-version }}" == "6.0" ] && [ ${{ matrix.python-version }} == '3.12' ]; then make docs-html; fi # test - name: test From a6533499ef33a27a2e4840b76023a09af3903750 Mon Sep 17 00:00:00 2001 From: Faakhir Zahid <110815427+Faakhir30@users.noreply.github.com> Date: Mon, 30 Sep 2024 17:47:35 +0500 Subject: [PATCH 04/26] added upload and download aliases in csv format. (#1813) * Added text/CSV content-type support to upload/download aliases in bulk. * Added aliases unit/integration tests for aliases service. * Added documentation --- docs/source/endpoints/aliases.md | 40 ++++- news/1812.feature | 1 + src/plone/restapi/services/aliases/add.py | 39 ++++- .../restapi/services/aliases/configure.zcml | 9 + src/plone/restapi/services/aliases/get.py | 58 ++++++- .../aliases_root_add_csv_format.req | 13 ++ .../aliases_root_add_csv_format.resp | 2 + .../aliases_root_get_csv_format.req | 3 + .../aliases_root_get_csv_format.resp | 6 + src/plone/restapi/tests/test_documentation.py | 53 ++++++ .../restapi/tests/test_services_aliases.py | 162 ++++++++++++++++++ 11 files changed, 370 insertions(+), 16 deletions(-) create mode 100644 news/1812.feature create mode 100644 src/plone/restapi/tests/http-examples/aliases_root_add_csv_format.req create mode 100644 src/plone/restapi/tests/http-examples/aliases_root_add_csv_format.resp create mode 100644 src/plone/restapi/tests/http-examples/aliases_root_get_csv_format.req create mode 100644 src/plone/restapi/tests/http-examples/aliases_root_get_csv_format.resp create mode 100644 src/plone/restapi/tests/test_services_aliases.py diff --git a/docs/source/endpoints/aliases.md b/docs/source/endpoints/aliases.md index 46f68342e1..da841c2e0a 100644 --- a/docs/source/endpoints/aliases.md +++ b/docs/source/endpoints/aliases.md @@ -70,9 +70,10 @@ Response: :language: http ``` -## Adding URL aliases in bulk +## Adding URL aliases in bulk via JSON -You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root`. **datetime** parameter is optional: +You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root` using a JSON payload. +**datetime** parameter is optional: ```{eval-rst} .. http:example:: curl httpie python-requests @@ -85,10 +86,26 @@ Response: :language: http ``` +## Adding URL aliases in bulk via CSV -## Listing all available aliases +You can add multiple URL aliases for multiple pages by sending a `POST` request to the `/@aliases` endpoint on site `root` using a CSV file. +**datetime** parameter is optional: -To list all aliases, send a `GET` request to the `/@aliases` endpoint on site `root`: +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/aliases_root_add_csv_format.req +``` + +Response: + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/aliases_root_add_csv_format.resp +:language: http +``` + + +## Listing all available aliases via JSON + +To list all aliases in JSON format, send a `GET` request to the `/@aliases` endpoint on site `root`: ```{eval-rst} .. http:example:: curl httpie python-requests @@ -101,6 +118,21 @@ Response: :language: http ``` +## Listing all available aliases via CSV + +To download all aliases as a CSV file, send a `GET` request to the `/@aliases` endpoint on site `root`: + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/aliases_root_get_csv_format.req +``` + +Response: + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/aliases_root_get_csv_format.resp +:language: http +``` + ## Filter aliases To search for specific aliases, send a `GET` request to the `/@aliases` endpoint on site `root` with a `q` parameter: diff --git a/news/1812.feature b/news/1812.feature new file mode 100644 index 0000000000..85727b674b --- /dev/null +++ b/news/1812.feature @@ -0,0 +1 @@ +Added create and fetch aliases in CSV format. @Faakhir30 diff --git a/src/plone/restapi/services/aliases/add.py b/src/plone/restapi/services/aliases/add.py index 555fbdbe0f..ab8af1ed2a 100644 --- a/src/plone/restapi/services/aliases/add.py +++ b/src/plone/restapi/services/aliases/add.py @@ -1,17 +1,24 @@ from DateTime import DateTime +from DateTime.interfaces import DateTimeError from plone.app.redirector.interfaces import IRedirectionStorage from plone.restapi import _ from plone.restapi.deserializer import json_body from plone.restapi.services import Service from Products.CMFPlone.controlpanel.browser.redirects import absolutize_path +from Products.CMFPlone.controlpanel.browser.redirects import RedirectsControlPanel +from Products.statusmessages.interfaces import IStatusMessage from zExceptions import BadRequest from zope.component import getMultiAdapter +from zope.component.hooks import getSite from zope.component import getUtility from zope.interface import alsoProvides from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse import plone.protect.interfaces +import logging + +logger = logging.getLogger("Plone") @implementer(IPublishTraverse) @@ -83,15 +90,35 @@ def edit_for_navigation_root(self, alias): class AliasesRootPost(Service): """Creates new aliases via controlpanel""" - def reply(self): - data = json_body(self.request) + def _reply_csv(self): + form = self.request.form + if not form.get("file"): + raise BadRequest("No file uploaded") + controlpanel = RedirectsControlPanel(self.context, self.request) storage = getUtility(IRedirectionStorage) - aliases = data.get("items", []) + status = IStatusMessage(self.request) + portal = getSite() + file = form["file"] + controlpanel.upload(file, portal, storage, status) + file.close() + + if err := status.show(): + if err[0].type == "error": + raise BadRequest(err[0].message) + elif err[0].type == "info": + logger.info(err[0].message) + return self.reply_no_content() + def reply(self): # Disable CSRF protection if "IDisableCSRFProtection" in dir(plone.protect.interfaces): alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) + if "multipart/form-data" in self.request.getHeader("Content-Type"): + return self._reply_csv() + storage = getUtility(IRedirectionStorage) + data = json_body(self.request) + aliases = data.get("items", []) for alias in aliases: redirection = alias.get("path") target = alias.get("redirect-to") @@ -113,7 +140,11 @@ def reply(self): date = alias.get("datetime", None) if date: - date = DateTime(date) + try: + date = DateTime(date) + except DateTimeError: + logger.warning("Failed to parse as DateTime: %s", date) + date = None storage.add(abs_redirection, abs_target, now=date, manual=True) diff --git a/src/plone/restapi/services/aliases/configure.zcml b/src/plone/restapi/services/aliases/configure.zcml index e3291cf161..c6499d117a 100644 --- a/src/plone/restapi/services/aliases/configure.zcml +++ b/src/plone/restapi/services/aliases/configure.zcml @@ -12,6 +12,15 @@ name="@aliases" /> + + Date: Tue, 1 Oct 2024 20:53:30 +0500 Subject: [PATCH 05/26] Fix logger initialization at add Allias by csv (#1821) * added upload and download aliases in csv format. * Update documentation * handle file closing * fix logger init * Add check to upload CSV file's content-type. --- src/plone/restapi/services/aliases/add.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/services/aliases/add.py b/src/plone/restapi/services/aliases/add.py index ab8af1ed2a..e54ccc0c67 100644 --- a/src/plone/restapi/services/aliases/add.py +++ b/src/plone/restapi/services/aliases/add.py @@ -18,7 +18,7 @@ import plone.protect.interfaces import logging -logger = logging.getLogger("Plone") +logger = logging.getLogger(__name__) @implementer(IPublishTraverse) @@ -94,11 +94,16 @@ def _reply_csv(self): form = self.request.form if not form.get("file"): raise BadRequest("No file uploaded") + + file = form["file"] + + if file.headers.get("Content-Type") not in ("text/csv", "application/csv"): + raise BadRequest("Uploaded file is not a valid CSV file") + controlpanel = RedirectsControlPanel(self.context, self.request) storage = getUtility(IRedirectionStorage) status = IStatusMessage(self.request) portal = getSite() - file = form["file"] controlpanel.upload(file, portal, storage, status) file.close() From 6e5e669ca11585ed69a5a02cd00175dfb5a73b46 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Wed, 2 Oct 2024 09:01:47 -0700 Subject: [PATCH 06/26] Fix spelling of prerequisites (#1822) * Fix spelling of prerequisites * News --- docs/source/contributing/index.md | 4 ++-- news/1822.documentation | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 news/1822.documentation diff --git a/docs/source/contributing/index.md b/docs/source/contributing/index.md index 1c0c9ac617..f442e71306 100644 --- a/docs/source/contributing/index.md +++ b/docs/source/contributing/index.md @@ -13,9 +13,9 @@ This section describes how to contribute to the `plone.restapi` project. It extends {doc}`plone:contributing/index`. -## Pre-requisites +## Prerequisites -Prepare your system by installing {ref}`plone:plone-pre-requisites-label`. +Prepare your system by installing {ref}`plone:plone-prerequisites-label`. ## Set up development environment diff --git a/news/1822.documentation b/news/1822.documentation new file mode 100644 index 0000000000..c37ab93222 --- /dev/null +++ b/news/1822.documentation @@ -0,0 +1 @@ +Fixed spelling of prerequisites. @stevepiercy From f68fab7ea3209a096423d8567fa7f70a93786c7a Mon Sep 17 00:00:00 2001 From: Faakhir Zahid <110815427+Faakhir30@users.noreply.github.com> Date: Thu, 3 Oct 2024 02:00:40 +0500 Subject: [PATCH 07/26] Fix invalid values in RelationListFieldSerializer. (#1818) * Fix invalid values in serializer. * Add test to check deleted relations * use to_id attribute to check content's existance. --- news/1818.bugfix | 1 + src/plone/restapi/serializer/relationfield.py | 14 +++++++++- .../tests/test_dxcontent_serializer.py | 28 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 news/1818.bugfix diff --git a/news/1818.bugfix b/news/1818.bugfix new file mode 100644 index 0000000000..54260238ec --- /dev/null +++ b/news/1818.bugfix @@ -0,0 +1 @@ +Fix response of `RelationListFieldSerializer` by filtering out invalid items. @Faakhir30 diff --git a/src/plone/restapi/serializer/relationfield.py b/src/plone/restapi/serializer/relationfield.py index 28483ce424..d54cadb9d1 100644 --- a/src/plone/restapi/serializer/relationfield.py +++ b/src/plone/restapi/serializer/relationfield.py @@ -33,4 +33,16 @@ class RelationChoiceFieldSerializer(DefaultFieldSerializer): @adapter(IRelationList, IDexterityContent, Interface) @implementer(IFieldSerializer) class RelationListFieldSerializer(DefaultFieldSerializer): - pass + def get_value(self, default=[]): + """Return field value reduced to list of non-broken Relationvalues. + + Args: + default (list, optional): Default field value. Defaults to empty list. + + Returns: + list: List of RelationValues + """ + value = super().get_value() + if not value: + return [] + return [el for el in value if el.to_id] diff --git a/src/plone/restapi/tests/test_dxcontent_serializer.py b/src/plone/restapi/tests/test_dxcontent_serializer.py index 6fd514956f..567aa01e62 100644 --- a/src/plone/restapi/tests/test_dxcontent_serializer.py +++ b/src/plone/restapi/tests/test_dxcontent_serializer.py @@ -27,6 +27,10 @@ from zope.component import provideAdapter from zope.component import queryUtility from zope.interface import Interface +from z3c.relationfield import RelationValue +from z3c.relationfield.event import _setRelation +from zope.component import getUtility +from zope.intid.interfaces import IIntIds from zope.publisher.interfaces.browser import IBrowserRequest from importlib import import_module @@ -191,6 +195,30 @@ def test_serializer_includes_expansion(self): "foo", ) + def test_serializer_excludes_deleted_relations(self): + + intids = getUtility(IIntIds) + self.portal.invokeFactory( + "DXTestDocument", + id="doc2", + ) + rel1 = RelationValue(intids.getId(self.portal.doc1)) + rel2 = RelationValue(intids.getId(self.portal.doc2)) + self.portal.doc1.test_relationlist_field = [ + rel1, + rel2, + ] + _setRelation(self.portal.doc1, "test_relationlist_field", rel1) + _setRelation(self.portal.doc1, "test_relationlist_field", rel2) + # delete doc2 to make sure we have a None value in the relation list + self.portal.manage_delObjects(["doc2"]) + + obj = self.serialize() + self.assertEqual(1, len(obj["test_relationlist_field"])) + self.assertEqual( + "http://nohost/plone/doc1", obj["test_relationlist_field"][0]["@id"] + ) + def test_get_is_folderish(self): obj = self.serialize() self.assertIn("is_folderish", obj) From b9a56e1930748da15351b90d502304ca17a0ac5e Mon Sep 17 00:00:00 2001 From: jackahl <44289551+jackahl@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:32:56 +0200 Subject: [PATCH 08/26] Use existing aliases controlpanel permissions for aliases endpoint (#1825) * Use exisitng aliases controlpanel permissions for aliases endpoint * Use ManagePortalAliases for alias delete endpoint on siteroot Co-authored-by: David Glick * Update news/1820.bugfix --------- Co-authored-by: David Glick --- news/1820.bugfix | 1 + src/plone/restapi/services/aliases/configure.zcml | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 news/1820.bugfix diff --git a/news/1820.bugfix b/news/1820.bugfix new file mode 100644 index 0000000000..8f2aeaaf4e --- /dev/null +++ b/news/1820.bugfix @@ -0,0 +1 @@ +Aliases endpoint: Use "Manage Portal Aliases" and "Manage Content Aliases" permissions. @jackahl diff --git a/src/plone/restapi/services/aliases/configure.zcml b/src/plone/restapi/services/aliases/configure.zcml index c6499d117a..b68d1050cb 100644 --- a/src/plone/restapi/services/aliases/configure.zcml +++ b/src/plone/restapi/services/aliases/configure.zcml @@ -8,7 +8,7 @@ accept="application/json,application/schema+json" factory=".get.AliasesGet" for="Products.CMFPlone.interfaces.IPloneSiteRoot" - permission="zope2.View" + permission="Products.CMFPlone.ManagePortalAliases" name="@aliases" /> @@ -17,7 +17,7 @@ accept="text/csv" factory=".get.AliasesGet" for="Products.CMFPlone.interfaces.IPloneSiteRoot" - permission="zope2.View" + permission="Products.CMFPlone.ManagePortalAliases" name="@aliases" /> @@ -26,7 +26,7 @@ accept="application/json,application/schema+json" factory=".get.AliasesGet" for="Products.CMFCore.interfaces.IContentish" - permission="zope2.View" + permission="Products.CMFPlone.ManageContextAliases" name="@aliases" /> @@ -34,7 +34,7 @@ method="POST" factory=".add.AliasesPost" for="*" - permission="cmf.ModifyPortalContent" + permission="Products.CMFPlone.ManageContextAliases" name="@aliases" /> @@ -42,7 +42,7 @@ method="POST" factory=".add.AliasesRootPost" for="Products.CMFPlone.interfaces.IPloneSiteRoot" - permission="cmf.ModifyPortalContent" + permission="Products.CMFPlone.ManagePortalAliases" name="@aliases" /> @@ -50,7 +50,7 @@ method="DELETE" factory=".delete.AliasesDelete" for="*" - permission="cmf.ModifyPortalContent" + permission="Products.CMFPlone.ManageContextAliases" name="@aliases" /> @@ -58,7 +58,7 @@ method="DELETE" factory=".delete.AliasesDelete" for="Products.CMFPlone.interfaces.IPloneSiteRoot" - permission="cmf.ModifyPortalContent" + permission="Products.CMFPlone.ManagePortalAliases" name="@aliases" /> From d6815a4f9326cc547c2fee4080a4c4070b72c3c8 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 18 Oct 2024 09:30:52 -0700 Subject: [PATCH 09/26] Add a way to check for backend support of version-dependent features via the site service (#1826) * Add a way to check for backend support of version-dependent features via the site service * lint & changelog --- news/1826.feature | 1 + src/plone/restapi/services/site/get.py | 14 ++++++++++++++ .../restapi/tests/http-examples/site_get.resp | 3 +++ 3 files changed, 18 insertions(+) create mode 100644 news/1826.feature diff --git a/news/1826.feature b/news/1826.feature new file mode 100644 index 0000000000..3f21fa89aa --- /dev/null +++ b/news/1826.feature @@ -0,0 +1 @@ +Site service: Indicate whether the site supports filtering URL aliases by date. @davisagli diff --git a/src/plone/restapi/services/site/get.py b/src/plone/restapi/services/site/get.py index 43840a8b87..4ab0a9801e 100644 --- a/src/plone/restapi/services/site/get.py +++ b/src/plone/restapi/services/site/get.py @@ -10,6 +10,7 @@ from Products.CMFPlone.interfaces import IImagingSchema from Products.CMFPlone.interfaces import ISiteSchema from Products.CMFPlone.utils import getSiteLogo +from Products.CMFPlone.controlpanel.browser.redirects import RedirectionSet from zope.component import adapter from zope.component import getMultiAdapter from zope.component import getUtility @@ -49,6 +50,7 @@ def __call__(self, expand=False): "plone.default_language": language_settings.default_language, "plone.available_languages": language_settings.available_languages, "plone.portal_timezone": self.plone_timezone(), + "features": self.features(), } ) @@ -73,6 +75,18 @@ def plone_timezone(self): return portal_timezone + def features(self): + """Indicates which features are supported by this site. + + This can be used by a client to check for version-dependent features. + """ + result = { + "filter_aliases_by_date": hasattr( + RedirectionSet, "supports_date_range_filtering" + ), + } + return result + class SiteGet(Service): def reply(self): diff --git a/src/plone/restapi/tests/http-examples/site_get.resp b/src/plone/restapi/tests/http-examples/site_get.resp index 28a4c11670..43a502cc1e 100644 --- a/src/plone/restapi/tests/http-examples/site_get.resp +++ b/src/plone/restapi/tests/http-examples/site_get.resp @@ -3,6 +3,9 @@ Content-Type: application/json { "@id": "http://localhost:55001/plone/@site", + "features": { + "filter_aliases_by_date": false + }, "plone.allowed_sizes": [ "huge 1600:65536", "great 1200:65536", From 344dd9c7d4ee822978e35bca7b073b1cb0c570dc Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Mon, 21 Oct 2024 01:45:24 +0200 Subject: [PATCH 10/26] Test event Occurrence serialization (#1811) * add a failing test * add a check for objects that are not able to be adapted to IContentListing * changelog * run black * run flake8 * test that Ocurrence serialization returns correct data * changelog * conditional tests * lint * restore * changelog * restore * rename changelog file --- news/1809.internal | 2 + .../restapi/tests/test_serializer_summary.py | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 news/1809.internal diff --git a/news/1809.internal b/news/1809.internal new file mode 100644 index 0000000000..ae905f0068 --- /dev/null +++ b/news/1809.internal @@ -0,0 +1,2 @@ +Test that recurrence serialization provides correct data +[erral] diff --git a/src/plone/restapi/tests/test_serializer_summary.py b/src/plone/restapi/tests/test_serializer_summary.py index 16b54f7b0c..7945792419 100644 --- a/src/plone/restapi/tests/test_serializer_summary.py +++ b/src/plone/restapi/tests/test_serializer_summary.py @@ -1,19 +1,31 @@ +from datetime import datetime +from datetime import timedelta from DateTime import DateTime from plone.app.contentlisting.interfaces import IContentListingObject +from plone.app.event.dx.traverser import OccurrenceTraverser from plone.app.testing import popGlobalRegistry from plone.app.testing import pushGlobalRegistry from plone.dexterity.utils import createContentInContainer +from plone.event.interfaces import IEvent +from plone.event.interfaces import IEventRecurrence from plone.restapi.interfaces import ISerializeToJsonSummary from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from plone.restapi.testing import register_static_uuid_utility from Products.CMFCore.utils import getToolByName from zope.component import getMultiAdapter from zope.component.hooks import getSite +from zope.interface import alsoProvides import Missing import unittest +try: + from plone.app.event.adapters import OccurrenceContentListingObject +except ImportError: + OccurrenceContentListingObject = None + + class TestSummarySerializers(unittest.TestCase): layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING @@ -203,3 +215,65 @@ def test_dx_type_summary(self): }, summary, ) + + +class TestSummarySerializerswithRecurrenceObjects(unittest.TestCase): + layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + pushGlobalRegistry(getSite()) + register_static_uuid_utility(prefix="c6dcbd55ab2746e199cd4ed458") + + behaviors = self.portal.portal_types.DXTestDocument.behaviors + behaviors = behaviors + ( + "plone.eventbasic", + "plone.eventrecurrence", + ) + self.portal.portal_types.DXTestDocument.behaviors = behaviors + + self.event = createContentInContainer( + self.portal, + "DXTestDocument", + id="doc1", + title="Lorem Ipsum event", + description="Description event", + start=datetime.now(), + end=datetime.now() + timedelta(hours=1), + recurrence="RRULE:FREQ=DAILY;COUNT=3", # see https://github.com/plone/plone.app.event/blob/master/plone/app/event/tests/base_setup.py + ) + + alsoProvides(self.event, IEvent) + alsoProvides(self.event, IEventRecurrence) + + def tearDown(self): + popGlobalRegistry(getSite()) + + @unittest.skipIf( + OccurrenceContentListingObject is not None, + "this test needs a plone.app.event version that does not include a IContentListingObject adapter", + ) + def test_dx_event_with_recurrence_old_version(self): + tomorrow = datetime.now() + timedelta(days=1) + tomorrow_str = tomorrow.strftime("%Y-%m-%d") + ot = OccurrenceTraverser(self.event, self.request) + ocurrence = ot.publishTraverse(self.request, tomorrow_str) + + with self.assertRaises(TypeError): + getMultiAdapter((ocurrence, self.request), ISerializeToJsonSummary)() + + @unittest.skipIf( + OccurrenceContentListingObject is None, + "this test needs a plone.app.event version that includes a IContentListingObject adapter", + ) + def test_dx_event_with_recurrence_new_version(self): + tomorrow = datetime.now() + timedelta(days=1) + tomorrow_str = tomorrow.strftime("%Y-%m-%d") + ot = OccurrenceTraverser(self.event, self.request) + ocurrence = ot.publishTraverse(self.request, tomorrow_str) + summary = getMultiAdapter((ocurrence, self.request), ISerializeToJsonSummary)() + + self.assertEqual(summary["start"], tomorrow_str) + self.assertEqual(summary["Title"], ocurrence.Title()) From 940228e976f5cbe558804960f9689e5f95e7305f Mon Sep 17 00:00:00 2001 From: David Glick Date: Sun, 20 Oct 2024 17:01:14 -0700 Subject: [PATCH 11/26] Revert "Test event Occurrence serialization (#1811)" This reverts commit 344dd9c7d4ee822978e35bca7b073b1cb0c570dc. --- news/1809.internal | 2 - .../restapi/tests/test_serializer_summary.py | 74 ------------------- 2 files changed, 76 deletions(-) delete mode 100644 news/1809.internal diff --git a/news/1809.internal b/news/1809.internal deleted file mode 100644 index ae905f0068..0000000000 --- a/news/1809.internal +++ /dev/null @@ -1,2 +0,0 @@ -Test that recurrence serialization provides correct data -[erral] diff --git a/src/plone/restapi/tests/test_serializer_summary.py b/src/plone/restapi/tests/test_serializer_summary.py index 7945792419..16b54f7b0c 100644 --- a/src/plone/restapi/tests/test_serializer_summary.py +++ b/src/plone/restapi/tests/test_serializer_summary.py @@ -1,31 +1,19 @@ -from datetime import datetime -from datetime import timedelta from DateTime import DateTime from plone.app.contentlisting.interfaces import IContentListingObject -from plone.app.event.dx.traverser import OccurrenceTraverser from plone.app.testing import popGlobalRegistry from plone.app.testing import pushGlobalRegistry from plone.dexterity.utils import createContentInContainer -from plone.event.interfaces import IEvent -from plone.event.interfaces import IEventRecurrence from plone.restapi.interfaces import ISerializeToJsonSummary from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from plone.restapi.testing import register_static_uuid_utility from Products.CMFCore.utils import getToolByName from zope.component import getMultiAdapter from zope.component.hooks import getSite -from zope.interface import alsoProvides import Missing import unittest -try: - from plone.app.event.adapters import OccurrenceContentListingObject -except ImportError: - OccurrenceContentListingObject = None - - class TestSummarySerializers(unittest.TestCase): layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING @@ -215,65 +203,3 @@ def test_dx_type_summary(self): }, summary, ) - - -class TestSummarySerializerswithRecurrenceObjects(unittest.TestCase): - layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING - - def setUp(self): - self.portal = self.layer["portal"] - self.request = self.layer["request"] - - pushGlobalRegistry(getSite()) - register_static_uuid_utility(prefix="c6dcbd55ab2746e199cd4ed458") - - behaviors = self.portal.portal_types.DXTestDocument.behaviors - behaviors = behaviors + ( - "plone.eventbasic", - "plone.eventrecurrence", - ) - self.portal.portal_types.DXTestDocument.behaviors = behaviors - - self.event = createContentInContainer( - self.portal, - "DXTestDocument", - id="doc1", - title="Lorem Ipsum event", - description="Description event", - start=datetime.now(), - end=datetime.now() + timedelta(hours=1), - recurrence="RRULE:FREQ=DAILY;COUNT=3", # see https://github.com/plone/plone.app.event/blob/master/plone/app/event/tests/base_setup.py - ) - - alsoProvides(self.event, IEvent) - alsoProvides(self.event, IEventRecurrence) - - def tearDown(self): - popGlobalRegistry(getSite()) - - @unittest.skipIf( - OccurrenceContentListingObject is not None, - "this test needs a plone.app.event version that does not include a IContentListingObject adapter", - ) - def test_dx_event_with_recurrence_old_version(self): - tomorrow = datetime.now() + timedelta(days=1) - tomorrow_str = tomorrow.strftime("%Y-%m-%d") - ot = OccurrenceTraverser(self.event, self.request) - ocurrence = ot.publishTraverse(self.request, tomorrow_str) - - with self.assertRaises(TypeError): - getMultiAdapter((ocurrence, self.request), ISerializeToJsonSummary)() - - @unittest.skipIf( - OccurrenceContentListingObject is None, - "this test needs a plone.app.event version that includes a IContentListingObject adapter", - ) - def test_dx_event_with_recurrence_new_version(self): - tomorrow = datetime.now() + timedelta(days=1) - tomorrow_str = tomorrow.strftime("%Y-%m-%d") - ot = OccurrenceTraverser(self.event, self.request) - ocurrence = ot.publishTraverse(self.request, tomorrow_str) - summary = getMultiAdapter((ocurrence, self.request), ISerializeToJsonSummary)() - - self.assertEqual(summary["start"], tomorrow_str) - self.assertEqual(summary["Title"], ocurrence.Title()) From 1a07d9016e470aeea7e4187a482b8714a43924d8 Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 23 Oct 2024 09:40:39 -0700 Subject: [PATCH 12/26] Preparing release 9.8.0 --- CHANGES.rst | 28 ++++++++++++++++++++++++++++ news/1808.bugfix | 2 -- news/1812.feature | 1 - news/1815.documentation | 1 - news/1816.bugfix | 2 -- news/1818.bugfix | 1 - news/1820.bugfix | 1 - news/1822.documentation | 1 - news/1826.feature | 1 - setup.py | 2 +- 10 files changed, 29 insertions(+), 11 deletions(-) delete mode 100644 news/1808.bugfix delete mode 100644 news/1812.feature delete mode 100644 news/1815.documentation delete mode 100644 news/1816.bugfix delete mode 100644 news/1818.bugfix delete mode 100644 news/1820.bugfix delete mode 100644 news/1822.documentation delete mode 100644 news/1826.feature diff --git a/CHANGES.rst b/CHANGES.rst index 7d5323edb8..03ec595954 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,34 @@ Changelog .. towncrier release notes start +9.8.0 (2024-10-23) +------------------ + +New features: + + +- Added create and fetch aliases in CSV format. @Faakhir30 (#1812) +- Site service: Indicate whether the site supports filtering URL aliases by date. @davisagli (#1826) + + +Bug fixes: + + +- Fix error getting allow_discussion value when p.a.discussion is not activated. + [maurits] (#1808) +- Fix incorrect condition for ``show_excluded_items`` setting in the ``@navigation`` API. + [mamico] (#1816) +- Fix response of `RelationListFieldSerializer` by filtering out invalid items. @Faakhir30 (#1818) +- Aliases endpoint: Use "Manage Portal Aliases" and "Manage Content Aliases" permissions. @jackahl (#1820) + + +Documentation: + + +- Use Plone Sphinx Theme for documentation. Build docs when there are changes to http-examples. @stevepiercy (#1815) +- Fixed spelling of prerequisites. @stevepiercy (#1822) + + 9.7.2 (2024-09-05) ------------------ diff --git a/news/1808.bugfix b/news/1808.bugfix deleted file mode 100644 index 7a8ab1df8e..0000000000 --- a/news/1808.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix error getting allow_discussion value when p.a.discussion is not activated. -[maurits] diff --git a/news/1812.feature b/news/1812.feature deleted file mode 100644 index 85727b674b..0000000000 --- a/news/1812.feature +++ /dev/null @@ -1 +0,0 @@ -Added create and fetch aliases in CSV format. @Faakhir30 diff --git a/news/1815.documentation b/news/1815.documentation deleted file mode 100644 index dd9891dfe5..0000000000 --- a/news/1815.documentation +++ /dev/null @@ -1 +0,0 @@ -Use Plone Sphinx Theme for documentation. Build docs when there are changes to http-examples. @stevepiercy diff --git a/news/1816.bugfix b/news/1816.bugfix deleted file mode 100644 index 2bb52fc581..0000000000 --- a/news/1816.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix incorrect condition for ``show_excluded_items`` setting in the ``@navigation`` API. -[mamico] diff --git a/news/1818.bugfix b/news/1818.bugfix deleted file mode 100644 index 54260238ec..0000000000 --- a/news/1818.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix response of `RelationListFieldSerializer` by filtering out invalid items. @Faakhir30 diff --git a/news/1820.bugfix b/news/1820.bugfix deleted file mode 100644 index 8f2aeaaf4e..0000000000 --- a/news/1820.bugfix +++ /dev/null @@ -1 +0,0 @@ -Aliases endpoint: Use "Manage Portal Aliases" and "Manage Content Aliases" permissions. @jackahl diff --git a/news/1822.documentation b/news/1822.documentation deleted file mode 100644 index c37ab93222..0000000000 --- a/news/1822.documentation +++ /dev/null @@ -1 +0,0 @@ -Fixed spelling of prerequisites. @stevepiercy diff --git a/news/1826.feature b/news/1826.feature deleted file mode 100644 index 3f21fa89aa..0000000000 --- a/news/1826.feature +++ /dev/null @@ -1 +0,0 @@ -Site service: Indicate whether the site supports filtering URL aliases by date. @davisagli diff --git a/setup.py b/setup.py index c9b76e9107..6858c5da23 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.7.3.dev0" +version = "9.8.0" if sys.version_info.major == 2: raise ValueError( From ffd2045042769ad1cb80e92a54fcb94e19da6cbf Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 23 Oct 2024 09:41:24 -0700 Subject: [PATCH 13/26] Back to development: 9.8.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6858c5da23..87192c8f72 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.0" +version = "9.8.1.dev0" if sys.version_info.major == 2: raise ValueError( From 203cfb3020ff1b418f252604ee36b0fb4640c927 Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 23 Oct 2024 13:18:36 -0700 Subject: [PATCH 14/26] Make sure CMFPlone's permissions are loaded (#1827) * Make sure CMFPlone's permissions are loaded * changelog --- news/1827.bugfix | 1 + src/plone/restapi/configure.zcml | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/1827.bugfix diff --git a/news/1827.bugfix b/news/1827.bugfix new file mode 100644 index 0000000000..98d430bbf2 --- /dev/null +++ b/news/1827.bugfix @@ -0,0 +1 @@ +Fix `ComponentLookupError` for `Products.CMFPlone.ManagePortalAliases` permission, which could happen depending on package load order. @davisagli diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index 2f79ec0c7a..8bd6c38c33 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -22,6 +22,7 @@ + Date: Wed, 23 Oct 2024 13:19:28 -0700 Subject: [PATCH 15/26] Preparing release 9.8.1 --- CHANGES.rst | 9 +++++++++ news/1827.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/1827.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 03ec595954..46e94d885b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +9.8.1 (2024-10-23) +------------------ + +Bug fixes: + + +- Fix `ComponentLookupError` for `Products.CMFPlone.ManagePortalAliases` permission, which could happen depending on package load order. @davisagli (#1827) + + 9.8.0 (2024-10-23) ------------------ diff --git a/news/1827.bugfix b/news/1827.bugfix deleted file mode 100644 index 98d430bbf2..0000000000 --- a/news/1827.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix `ComponentLookupError` for `Products.CMFPlone.ManagePortalAliases` permission, which could happen depending on package load order. @davisagli diff --git a/setup.py b/setup.py index 87192c8f72..bec87a369f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.1.dev0" +version = "9.8.1" if sys.version_info.major == 2: raise ValueError( From dcdb41bcab60b4bc32fa5c685f146b40d88c33bd Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 23 Oct 2024 13:20:02 -0700 Subject: [PATCH 16/26] Back to development: 9.8.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bec87a369f..eff315196c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.1" +version = "9.8.2.dev0" if sys.version_info.major == 2: raise ValueError( From b9ca1f9afa2d26193df8513f14cb159a2a83a012 Mon Sep 17 00:00:00 2001 From: Teodor Voicu <104510089+tedw87@users.noreply.github.com> Date: Wed, 30 Oct 2024 13:43:55 -0700 Subject: [PATCH 17/26] Handle parentheses search queries (#1828) * escape parantheses in query * reformat code * add changelog * Update news/1828.bugfix Co-authored-by: Steve Piercy * Update src/plone/restapi/search/handler.py Co-authored-by: Steve Piercy * add tests for searching with parantheses * format code * run black to format code * Update news/1828.bugfix --------- Co-authored-by: Steve Piercy Co-authored-by: David Glick --- news/1828.bugfix | 1 + src/plone/restapi/search/handler.py | 11 ++++++++++- src/plone/restapi/tests/test_search.py | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 news/1828.bugfix diff --git a/news/1828.bugfix b/news/1828.bugfix new file mode 100644 index 0000000000..dfc2a5ef4e --- /dev/null +++ b/news/1828.bugfix @@ -0,0 +1 @@ +`@search` service: Remove parentheses from search query. @tedw87 \ No newline at end of file diff --git a/src/plone/restapi/search/handler.py b/src/plone/restapi/search/handler.py index 8764a773df..2a362e05d5 100644 --- a/src/plone/restapi/search/handler.py +++ b/src/plone/restapi/search/handler.py @@ -75,6 +75,10 @@ def _constrain_query_by_path(self, query): path = "/".join(self.context.getPhysicalPath()) query["path"]["query"] = path + def quote_chars(self, query): + # Remove parentheses from the query + return query.replace("(", " ").replace(")", " ").strip() + def search(self, query=None): if query is None: query = {} @@ -93,6 +97,12 @@ def search(self, query=None): if use_site_search_settings: query = self.filter_query(query) + if "SearchableText" in query: + # Sanitize SearchableText by removing parentheses + query["SearchableText"] = self.quote_chars(query["SearchableText"]) + if not query["SearchableText"] or query["SearchableText"] == "*": + return [] + self._constrain_query_by_path(query) query = self._parse_query(query) @@ -100,7 +110,6 @@ def search(self, query=None): results = getMultiAdapter((lazy_resultset, self.request), ISerializeToJson)( fullobjects=fullobjects ) - return results def filter_types(self, types): diff --git a/src/plone/restapi/tests/test_search.py b/src/plone/restapi/tests/test_search.py index e4ddb4c38d..84b6e0b480 100644 --- a/src/plone/restapi/tests/test_search.py +++ b/src/plone/restapi/tests/test_search.py @@ -151,6 +151,29 @@ def test_search_on_context_constrains_query_by_path(self): set(result_paths(response.json())), ) + def test_search_with_parentheses(self): + query = {"SearchableText": "("} + response = self.api_session.get("/@search", params=query) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), [], "Expected no items for query with only parentheses" + ) + + query = {"SearchableText": ")"} + response = self.api_session.get("/@search", params=query) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), [], "Expected no items for query with only parentheses" + ) + + query = {"SearchableText": "lorem(ipsum)"} + response = self.api_session.get("/@search", params=query) + self.assertEqual(response.status_code, 200) + items = [item["title"] for item in response.json().get("items", [])] + self.assertIn( + "Lorem Ipsum", items, "Expected 'Lorem Ipsum' to be found in search results" + ) + def test_search_in_vhm(self): # Install a Virtual Host Monster if "virtual_hosting" not in self.app.objectIds(): From 3f72b6d83c3a2a4ae42dd1d8a0db551a2bc49433 Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 30 Oct 2024 13:54:38 -0700 Subject: [PATCH 18/26] Preparing release 9.8.2 --- CHANGES.rst | 9 +++++++++ news/1828.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/1828.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 46e94d885b..4954960da9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +9.8.2 (2024-10-30) +------------------ + +Bug fixes: + + +- `@search` service: Remove parentheses from search query. @tedw87 (#1828) + + 9.8.1 (2024-10-23) ------------------ diff --git a/news/1828.bugfix b/news/1828.bugfix deleted file mode 100644 index dfc2a5ef4e..0000000000 --- a/news/1828.bugfix +++ /dev/null @@ -1 +0,0 @@ -`@search` service: Remove parentheses from search query. @tedw87 \ No newline at end of file diff --git a/setup.py b/setup.py index eff315196c..2f2f82819f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.2.dev0" +version = "9.8.2" if sys.version_info.major == 2: raise ValueError( From e7bd7b7ed842072f659ee34e8fcc2ced4eba5a8c Mon Sep 17 00:00:00 2001 From: David Glick Date: Wed, 30 Oct 2024 13:55:16 -0700 Subject: [PATCH 19/26] Back to development: 9.8.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2f2f82819f..183ac1eedb 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.2" +version = "9.8.3.dev0" if sys.version_info.major == 2: raise ValueError( From e2d4e70fa475bd9ccb994af52632c8ef45f76279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Thu, 31 Oct 2024 20:23:30 -0300 Subject: [PATCH 20/26] Fixes Plone Site serialization when there is a field with read_permision set (#1831) * Fixes Plone Site serialization when there is a field with read_permission set (Fixes #1830) * Update news/1830.bugfix --------- Co-authored-by: David Glick --- news/1830.bugfix | 1 + src/plone/restapi/serializer/site.py | 10 ++++++---- src/plone/restapi/tests/dxtypes.py | 3 +++ src/plone/restapi/tests/test_site_serializer.py | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 news/1830.bugfix diff --git a/news/1830.bugfix b/news/1830.bugfix new file mode 100644 index 0000000000..df8dfb81c3 --- /dev/null +++ b/news/1830.bugfix @@ -0,0 +1 @@ +Fixed Plone Site serialization when there is a field with read_permission set. @ericof \ No newline at end of file diff --git a/src/plone/restapi/serializer/site.py b/src/plone/restapi/serializer/site.py index 11c5a0a2a3..65081fd2ea 100644 --- a/src/plone/restapi/serializer/site.py +++ b/src/plone/restapi/serializer/site.py @@ -4,15 +4,17 @@ from plone.dexterity.utils import iterSchemata from plone.restapi.batching import HypermediaBatch from plone.restapi.bbb import IPloneSiteRoot -from plone.restapi.blocks import visit_blocks, iter_block_transform_handlers +from plone.restapi.blocks import iter_block_transform_handlers +from plone.restapi.blocks import visit_blocks +from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import IFieldSerializer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISerializeToJsonSummary -from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.serializer.converters import json_compatible +from plone.restapi.serializer.dxcontent import get_allow_discussion_value from plone.restapi.serializer.expansion import expandable_elements -from plone.restapi.services.locking import lock_info from plone.restapi.serializer.utils import get_portal_type_title +from plone.restapi.services.locking import lock_info from plone.supermodel.utils import mergedTaggedValueDict from Products.CMFCore.utils import getToolByName from zope.component import adapter @@ -23,7 +25,6 @@ from zope.interface import Interface from zope.schema import getFields from zope.security.interfaces import IPermission -from plone.restapi.serializer.dxcontent import get_allow_discussion_value import json @@ -39,6 +40,7 @@ class SerializeSiteRootToJson: def __init__(self, context, request): self.context = context self.request = request + self.permission_cache = {} def _build_query(self): path = "/".join(self.context.getPhysicalPath()) diff --git a/src/plone/restapi/tests/dxtypes.py b/src/plone/restapi/tests/dxtypes.py index b3632d41d9..c7f8ef148f 100644 --- a/src/plone/restapi/tests/dxtypes.py +++ b/src/plone/restapi/tests/dxtypes.py @@ -312,6 +312,9 @@ class ITestBehavior(model.Schema): test_behavior_field = schema.TextLine(required=False) # Add nav_title to test if it gets substituted in Navigation service nav_title = schema.TextLine(required=False) + # Add a field with read permission set + test_secure_field = schema.TextLine(required=False) + read_permission(test_secure_field="cmf.ManagePortal") @provider(IFormFieldProvider) diff --git a/src/plone/restapi/tests/test_site_serializer.py b/src/plone/restapi/tests/test_site_serializer.py index 34174a7629..60de5ffffd 100644 --- a/src/plone/restapi/tests/test_site_serializer.py +++ b/src/plone/restapi/tests/test_site_serializer.py @@ -38,6 +38,7 @@ def setUp(self): if fti is not None: behavior_list = [a for a in fti.behaviors] behavior_list.append("volto.blocks") + behavior_list.append("tests.restapi.test_behavior") fti.behaviors = tuple(behavior_list) # Invalidating the cache is required for the FTI to be applied # on the existing object From 741bd1cdfd5b27440554979d54510eae42d3ae2c Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 1 Nov 2024 14:25:01 -0700 Subject: [PATCH 21/26] Preparing release 9.8.3 --- CHANGES.rst | 9 +++++++++ news/1830.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/1830.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 4954960da9..3e6cc99b82 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +9.8.3 (2024-11-01) +------------------ + +Bug fixes: + + +- Fixed Plone Site serialization when there is a field with read_permission set. @ericof (#1830) + + 9.8.2 (2024-10-30) ------------------ diff --git a/news/1830.bugfix b/news/1830.bugfix deleted file mode 100644 index df8dfb81c3..0000000000 --- a/news/1830.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed Plone Site serialization when there is a field with read_permission set. @ericof \ No newline at end of file diff --git a/setup.py b/setup.py index 183ac1eedb..03ea5095b5 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.3.dev0" +version = "9.8.3" if sys.version_info.major == 2: raise ValueError( From e63416968eb390f42df50c1025395d1b3baf7f60 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 1 Nov 2024 14:25:51 -0700 Subject: [PATCH 22/26] Back to development: 9.8.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 03ea5095b5..dc007c4c1f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.3" +version = "9.8.4.dev0" if sys.version_info.major == 2: raise ValueError( From 636ef37ecf1fce3de17c80fa29e6cac7e6ffbcac Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 5 Nov 2024 12:55:02 -0800 Subject: [PATCH 23/26] Fix handling of errors in aliases CSV upload (#1838) * Fix handling of errors in aliases CSV upload * changelog --- news/1837.bugfix | 1 + src/plone/restapi/services/aliases/add.py | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 news/1837.bugfix diff --git a/news/1837.bugfix b/news/1837.bugfix new file mode 100644 index 0000000000..08502b6c36 --- /dev/null +++ b/news/1837.bugfix @@ -0,0 +1 @@ +URL Management control panel: Fix error handling in CSV upload. @davisagli diff --git a/src/plone/restapi/services/aliases/add.py b/src/plone/restapi/services/aliases/add.py index e54ccc0c67..2894d25b22 100644 --- a/src/plone/restapi/services/aliases/add.py +++ b/src/plone/restapi/services/aliases/add.py @@ -101,12 +101,22 @@ def _reply_csv(self): raise BadRequest("Uploaded file is not a valid CSV file") controlpanel = RedirectsControlPanel(self.context, self.request) + csv_errors = controlpanel.csv_errors = [] storage = getUtility(IRedirectionStorage) status = IStatusMessage(self.request) portal = getSite() controlpanel.upload(file, portal, storage, status) file.close() + if csv_errors: + self.request.response.setHeader("Content-Type", "application/json") + self.request.response.setStatus(BadRequest) + return { + "type": "BadRequest", + "message": f"Found {len(csv_errors)} errors in CSV file.", + # Skip first item which is a notice about the delimiter + "csv_errors": csv_errors[1:], + } if err := status.show(): if err[0].type == "error": raise BadRequest(err[0].message) From 8297ad9f103844509f2ef2e21b05edb3cbc88a56 Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 5 Nov 2024 14:07:54 -0800 Subject: [PATCH 24/26] Preparing release 9.8.4 --- CHANGES.rst | 9 +++++++++ news/1837.bugfix | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/1837.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index 3e6cc99b82..252e57258f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +9.8.4 (2024-11-05) +------------------ + +Bug fixes: + + +- URL Management control panel: Fix error handling in CSV upload. @davisagli (#1837) + + 9.8.3 (2024-11-01) ------------------ diff --git a/news/1837.bugfix b/news/1837.bugfix deleted file mode 100644 index 08502b6c36..0000000000 --- a/news/1837.bugfix +++ /dev/null @@ -1 +0,0 @@ -URL Management control panel: Fix error handling in CSV upload. @davisagli diff --git a/setup.py b/setup.py index dc007c4c1f..2cf2760837 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.4.dev0" +version = "9.8.4" if sys.version_info.major == 2: raise ValueError( From 9f0feffad288f18e5fa3d3f63094614971d1d164 Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 5 Nov 2024 14:09:56 -0800 Subject: [PATCH 25/26] Back to development: 9.8.5 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2cf2760837..69b2ff7184 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.8.4" +version = "9.8.5.dev0" if sys.version_info.major == 2: raise ValueError( From 4f3bb4f1c656295f609a9aaea4540325d1b55b8e Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Fri, 8 Nov 2024 22:19:39 +0100 Subject: [PATCH 26/26] Change login name after changing email in sites where "use email as login" is enabled (#1836) * add test case * test case for managers changing password * fix: changing email should change login name * changelog --------- Co-authored-by: Jens W. Klein --- news/1835.bugfix | 2 + src/plone/restapi/services/users/update.py | 9 + .../restapi/tests/test_services_users.py | 227 ++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 news/1835.bugfix diff --git a/news/1835.bugfix b/news/1835.bugfix new file mode 100644 index 0000000000..e8b78b308a --- /dev/null +++ b/news/1835.bugfix @@ -0,0 +1,2 @@ +Fix log in after changing email when "email as login" is enabled +[erral] diff --git a/src/plone/restapi/services/users/update.py b/src/plone/restapi/services/users/update.py index 428727d1c3..3d7d3b724b 100644 --- a/src/plone/restapi/services/users/update.py +++ b/src/plone/restapi/services/users/update.py @@ -104,6 +104,11 @@ def reply(self): self.set_member_portrait(user, value) user.setMemberProperties(mapping={key: value}, force_empty=True) + if security.use_email_as_login and "email" in user_settings_to_update: + value = user_settings_to_update["email"] + pas = getToolByName(self.context, "acl_users") + pas.updateLoginName(user.getId(), value) + roles = user_settings_to_update.get("roles", {}) if roles: to_add = [key for key, enabled in roles.items() if enabled] @@ -142,6 +147,10 @@ def reply(self): self.set_member_portrait(user, value) user.setMemberProperties(mapping={key: value}, force_empty=True) + if security.use_email_as_login and "email" in user_settings_to_update: + value = user_settings_to_update["email"] + set_own_login_name(user, value) + else: if self._is_anonymous: return self._error( diff --git a/src/plone/restapi/tests/test_services_users.py b/src/plone/restapi/tests/test_services_users.py index b203743b3e..3d80320769 100644 --- a/src/plone/restapi/tests/test_services_users.py +++ b/src/plone/restapi/tests/test_services_users.py @@ -1436,3 +1436,230 @@ def test_siteadm_not_change_manager_email(self): self.assertEqual( "manager@example.com", api.user.get(userid="manager").getProperty("email") ) + + def test_manager_changes_email_when_login_with_email(self): + """test that when login with email is enabled and a manager changes a user's email + they can log in with the new email + """ + # enable use_email_as_login + security_settings = getAdapter(self.portal, ISecuritySchema) + security_settings.use_email_as_login = True + transaction.commit() + # Create a user + response = self.api_session.post( + "/@users", + json={ + "email": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(response.ok) + userid = response.json()["id"] + + transaction.commit() + anon_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(anon_response.ok) + + email_change_response = self.api_session.patch( + f"/@users/{userid}", + json={ + "email": "new_email@example.com", + }, + ) + self.assertTrue(email_change_response.ok) + new_login_with_old_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertFalse(new_login_with_old_email_response.ok) + new_login_with_new_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "new_email@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(new_login_with_new_email_response.ok) + + def test_user_changes_email_when_login_with_email(self): + """test that when login with email is enabled and the user changes their email + they can log in with the new email + """ + # enable use_email_as_login + security_settings = getAdapter(self.portal, ISecuritySchema) + security_settings.use_email_as_login = True + transaction.commit() + # Create a user + response = self.api_session.post( + "/@users", + json={ + "email": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(response.ok) + userid = response.json()["id"] + + transaction.commit() + anon_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(anon_response.ok) + auth_token = anon_response.json().get("token") + + user_api_session = RelativeSession(self.portal_url, test=self) + user_api_session.headers.update({"Accept": "application/json"}) + user_api_session.headers.update({"Authorization": f"Bearer {auth_token}"}) + + email_change_response = user_api_session.patch( + f"/@users/{userid}", + json={"email": "new_email@example.com"}, + ) + + self.assertTrue(email_change_response.ok) + new_login_with_old_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertFalse(new_login_with_old_email_response.ok) + new_login_with_new_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "new_email@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(new_login_with_new_email_response.ok) + + def test_manager_changes_email_when_login_with_email_and_uuid_userids(self): + """test that when login with email is enabled and a manager changes a user's email + they can log in with the new email. + + The site is configured to save userids as uuid + + """ + # enable use_email_as_login + security_settings = getAdapter(self.portal, ISecuritySchema) + security_settings.use_email_as_login = True + security_settings.use_uuid_as_userid = True + transaction.commit() + # Create a user + response = self.api_session.post( + "/@users", + json={ + "email": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(response.ok) + userid = response.json()["id"] + transaction.commit() + anon_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(anon_response.ok) + + email_change_response = self.api_session.patch( + f"/@users/{userid}", + json={ + "email": "new_email@example.com", + }, + ) + self.assertTrue(email_change_response.ok) + new_login_with_old_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertFalse(new_login_with_old_email_response.ok) + new_login_with_new_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "new_email@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(new_login_with_new_email_response.ok) + + def test_user_changes_email_when_login_with_email_and_uuid_userids(self): + """test that when login with email is enabled and the user changes their email + they can log in with the new email + + The site is configured to save userids as uuid + + """ + # enable use_email_as_login + security_settings = getAdapter(self.portal, ISecuritySchema) + security_settings.use_email_as_login = True + security_settings.use_uuid_as_userid = True + + transaction.commit() + # Create a user + response = self.api_session.post( + "/@users", + json={ + "email": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(response.ok) + userid = response.json()["id"] + transaction.commit() + anon_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(anon_response.ok) + auth_token = anon_response.json().get("token") + + user_api_session = RelativeSession(self.portal_url, test=self) + user_api_session.headers.update({"Accept": "application/json"}) + user_api_session.headers.update({"Authorization": f"Bearer {auth_token}"}) + + email_change_response = user_api_session.patch( + f"/@users/{userid}", + json={"email": "new_email@example.com"}, + ) + + self.assertTrue(email_change_response.ok) + new_login_with_old_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "howard.zinn@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertFalse(new_login_with_old_email_response.ok) + new_login_with_new_email_response = self.anon_api_session.post( + "/@login", + json={ + "login": "new_email@example.com", + "password": TEST_USER_PASSWORD, + }, + ) + self.assertTrue(new_login_with_new_email_response.ok)