diff --git a/last_commit.txt b/last_commit.txt index d1d4ba8a6c..524bc77bdd 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,50 +1,496 @@ -Repository: plone.restapi - - -Branch: refs/heads/master -Date: 2020-03-15T18:41:30+01:00 -Author: Timo Stollenwerk (tisto) -Commit: https://github.com/plone/plone.restapi/commit/ea3bf8713a7a31be2520f776c1747256614c95ff - -Upgrade to Plone 5.2.1 (#883) - -* Upgrade to Plone 5.2.1 - -* Do not pin release related pkgs. - -* Remove sphinx version pins. - -* zest.releaser=6.20.1 - -* Pin sphinx for Plone 4.3. - -* Run sphinxbuilder for py 3.7 only. sphinx does not work on py 3.8 yet - -* Update docs to Plone 5.2.1. - -* Pin Pygments = 2.5.1 for Plone 4.3 - -* No sphinx on py 3.8 - -* Pin releaser/sphinx on 5.1 - -* Remove commented out pins. - -* Move plone.schema pin to 4.3/5.1 - -* Remove remaining versions.cfg pins. - -Files changed: -M .travis.yml -M plone-4.3.x.cfg -M plone-5.1.x.cfg -M plone-5.2.x.cfg -M src/plone/restapi/tests/http-examples/history_get.resp -M src/plone/restapi/tests/http-examples/registry_get_list.resp -M src/plone/restapi/tests/http-examples/translated_messages_object_history.resp -M src/plone/restapi/tests/http-examples/vocabularies.resp -M test-no-uncommitted-doc-changes.in -M versions.cfg - -b'diff --git a/.travis.yml b/.travis.yml\nindex a0b24e58..eb5fb8bd 100644\n--- a/.travis.yml\n+++ b/.travis.yml\n@@ -53,10 +53,10 @@ install:\n script:\n - bin/code-analysis\n - if [ -f "bin/black" ]; then bin/black src/ --check ; fi\n- - bin/sphinxbuilder\n+ - if [ "$PLONE_VERSION" == "5.2.x" ] && [ $TRAVIS_PYTHON_VERSION == \'3.7\' ]; then bin/sphinxbuilder ; fi\n - bin/test\n - bin/test-no-uncommitted-doc-changes\n- - bin/test-no-sphinx-warnings\n+ - if [ "$PLONE_VERSION" == "5.2.x" ] && [ $TRAVIS_PYTHON_VERSION == \'3.7\' ]; then bin/test-no-sphinx-warnings ; fi\n after_success:\n - bin/test-coverage\n - pip install coverage==3.7.1 coveralls\ndiff --git a/plone-4.3.x.cfg b/plone-4.3.x.cfg\nindex 9af1ade3..f0a28357 100644\n--- a/plone-4.3.x.cfg\n+++ b/plone-4.3.x.cfg\n@@ -15,3 +15,25 @@ six = 1.11.0\n # Required for Python 2.7 compatibility\n more-itertools = <6.0.0\n zipp = >=0.5, <2a\n+\n+# plone.restapi specific\n+plone.schema = 1.2.0\n+\n+# zest.releaser\n+zest.releaser = 6.20.1\n+twine = 1.11.0\n+requests = 2.18.4\n+towncrier = 19.2.0\n+zestreleaser.towncrier = 1.1.0\n+# docutils = 0.13.1\n+\n+# Sphinx\n+Sphinx = 1.6.5\n+docutils = 0.14\n+Pygments = 2.5.1\n+sphinxcontrib-httpexample = 0.7.0\n+sphinxcontrib-httpdomain = 1.5.0\n+sphinx-rtd-theme = 0.2.4\n+Jinja2 = 2.10\n+Babel = 2.5.1\n+astunparse = 1.6.2\n\\ No newline at end of file\ndiff --git a/plone-5.1.x.cfg b/plone-5.1.x.cfg\nindex 7c225d18..2fa89a63 100644\n--- a/plone-5.1.x.cfg\n+++ b/plone-5.1.x.cfg\n@@ -6,4 +6,26 @@ extends =\n \n [versions]\n # fixes: SyntaxError: invalid syntax (more.py, line 340)\n-zipp = 0.5.2\n\\ No newline at end of file\n+zipp = 0.5.2\n+\n+# plone.restapi specific\n+plone.schema = 1.2.0\n+\n+# zest.releaser\n+zest.releaser = 6.20.1\n+twine = 1.11.0\n+requests = 2.18.4\n+towncrier = 19.2.0\n+zestreleaser.towncrier = 1.1.0\n+# docutils = 0.13.1\n+\n+# Sphinx\n+Sphinx = 1.6.5\n+docutils = 0.14\n+Pygments = 2.5.1\n+sphinxcontrib-httpexample = 0.7.0\n+sphinxcontrib-httpdomain = 1.5.0\n+sphinx-rtd-theme = 0.2.4\n+Jinja2 = 2.10\n+Babel = 2.5.1\n+astunparse = 1.6.2\n\\ No newline at end of file\ndiff --git a/plone-5.2.x.cfg b/plone-5.2.x.cfg\nindex dcb0452a..f551d422 100644\n--- a/plone-5.2.x.cfg\n+++ b/plone-5.2.x.cfg\n@@ -1,7 +1,7 @@\n [buildout]\n extends =\n base.cfg\n- http://dist.plone.org/release/5.2/versions.cfg\n+ http://dist.plone.org/release/5.2.1/versions.cfg\n versions.cfg\n find-links += http://dist.plone.org/thirdparty/\n versions=versions\ndiff --git a/src/plone/restapi/tests/http-examples/history_get.resp b/src/plone/restapi/tests/http-examples/history_get.resp\nindex 2fc40e7a..7aec16f9 100644\n--- a/src/plone/restapi/tests/http-examples/history_get.resp\n+++ b/src/plone/restapi/tests/http-examples/history_get.resp\n@@ -22,9 +22,9 @@ Content-Type: application/json\n "action": "Create", \n "actor": {\n "@id": "http://localhost:55001/plone/@users/test_user_1_", \n- "fullname": "", \n+ "fullname": "test_user_1_", \n "id": "test_user_1_", \n- "username": "test-user"\n+ "username": null\n }, \n "comments": "", \n "review_state": "private", \ndiff --git a/src/plone/restapi/tests/http-examples/registry_get_list.resp b/src/plone/restapi/tests/http-examples/registry_get_list.resp\nindex 9a4dffae..33df71b6 100644\n--- a/src/plone/restapi/tests/http-examples/registry_get_list.resp\n+++ b/src/plone/restapi/tests/http-examples/registry_get_list.resp\n@@ -6,7 +6,7 @@ Content-Type: application/json\n "batching": {\n "@id": "http://localhost:55001/plone/@registry", \n "first": "http://localhost:55001/plone/@registry?b_start=0", \n- "last": "http://localhost:55001/plone/@registry?b_start=1675", \n+ "last": "http://localhost:55001/plone/@registry?b_start=1750", \n "next": "http://localhost:55001/plone/@registry?b_start=25"\n }, \n "items": [\n@@ -403,5 +403,5 @@ Content-Type: application/json\n "value": false\n }\n ], \n- "items_total": 1686\n+ "items_total": 1775\n }\n\\ No newline at end of file\ndiff --git a/src/plone/restapi/tests/http-examples/translated_messages_object_history.resp b/src/plone/restapi/tests/http-examples/translated_messages_object_history.resp\nindex 01a9f0c5..ba5e62be 100644\n--- a/src/plone/restapi/tests/http-examples/translated_messages_object_history.resp\n+++ b/src/plone/restapi/tests/http-examples/translated_messages_object_history.resp\n@@ -22,9 +22,9 @@ Content-Type: application/json\n "action": "Crear", \n "actor": {\n "@id": "http://localhost:55001/plone/@users/test_user_1_", \n- "fullname": "", \n+ "fullname": "test_user_1_", \n "id": "test_user_1_", \n- "username": "test-user"\n+ "username": null\n }, \n "comments": "", \n "review_state": "private", \ndiff --git a/src/plone/restapi/tests/http-examples/vocabularies.resp b/src/plone/restapi/tests/http-examples/vocabularies.resp\nindex 697b9f97..bee7edf1 100644\n--- a/src/plone/restapi/tests/http-examples/vocabularies.resp\n+++ b/src/plone/restapi/tests/http-examples/vocabularies.resp\n@@ -10,6 +10,10 @@ Content-Type: application/json\n "@id": "http://localhost:55001/plone/@vocabularies/plone.contentrules.events", \n "title": "plone.contentrules.events"\n }, \n+ {\n+ "@id": "http://localhost:55001/plone/@vocabularies/Behaviors", \n+ "title": "Behaviors"\n+ }, \n {\n "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.AvailableContentLanguages", \n "title": "plone.app.vocabularies.AvailableContentLanguages"\n@@ -146,10 +150,6 @@ Content-Type: application/json\n "@id": "http://localhost:55001/plone/@vocabularies/plone.schemaeditor.VocabulariesVocabulary", \n "title": "plone.schemaeditor.VocabulariesVocabulary"\n }, \n- {\n- "@id": "http://localhost:55001/plone/@vocabularies/Behaviors", \n- "title": "Behaviors"\n- }, \n {\n "@id": "http://localhost:55001/plone/@vocabularies/plone.formwidget.relations.cmfcontentsearch", \n "title": "plone.formwidget.relations.cmfcontentsearch"\ndiff --git a/test-no-uncommitted-doc-changes.in b/test-no-uncommitted-doc-changes.in\nindex d49188bb..e21fe30b 100644\n--- a/test-no-uncommitted-doc-changes.in\n+++ b/test-no-uncommitted-doc-changes.in\n@@ -13,10 +13,12 @@ function red {\n echo "$RED $1 $RESET"\n }\n \n-if [ "$PLONE_VERSION" = "4.3.x" ] || [ "$PLONE_VERSION" = "5.0.x" ] || [ "$PLONE_VERSION" = "5.1.x" ] || [ $TRAVIS_PYTHON_VERSION == \'2.7\' ]; then\n+if [ "$PLONE_VERSION" == "5.2.x" ] && [ $TRAVIS_PYTHON_VERSION == \'3.7\' ]; then\n+ echo "Running check for undocumented changes for Plone 5.2.x on Python 3.7"\n+else\n # request/response dumps have known differences for Plone 5\n # => skip, we can\'t have the Plone 5 build fail because of those\n- echo "Skipping checks for undocumented changes for Plone 4, 5.0.x, and 5.1.x."\n+ echo "Skipping checks for undocumented changes for everything except Plone 5.2.x on Python 3.7"\n exit 0\n fi\n \ndiff --git a/versions.cfg b/versions.cfg\nindex 8086ef94..361e6b8d 100644\n--- a/versions.cfg\n+++ b/versions.cfg\n@@ -2,38 +2,15 @@\n # Buildout\n setuptools =\n zc.buildout =\n-zc.recipe.egg = 2.0.3\n+# zc.recipe.egg = 2.0.3\n \n # plone.recipe.varnish\n-plone.recipe.varnish = 1.3\n+# plone.recipe.varnish = 1.3\n \n # Code-analysis\n # plone.recipe.codeanalysis = 3.0.1\n # coverage = 3.7.1\n # pep8 = 1.7.1\n-flake8 = 3.7.9\n+# flake8 = 3.7.9\n # flake8-coding = 1.3.2\n-pycodestyle = 2.5.0\n-\n-# Release\n-zest.releaser = 6.17.0\n-twine = 1.11.0\n-requests = 2.18.4\n-towncrier = 19.2.0\n-zestreleaser.towncrier = 1.1.0\n-docutils = 0.13.1\n-\n-# Sphinx\n-Sphinx = 1.6.5\n-docutils = 0.14\n-Pygments = 2.2.0\n-sphinxcontrib-httpexample = 0.7.0\n-sphinxcontrib-httpdomain = 1.5.0\n-sphinx-rtd-theme = 0.2.4\n-Jinja2 = 2.10\n-Babel = 2.5.1\n-astunparse = 1.6.2\n-\n-# plone.restapi specific\n-plone.schema = 1.2.0\n-\n+# pycodestyle = 2.5.0\n' +Repository: plone.app.discussion + + +Branch: refs/heads/master +Date: 2020-01-08T12:22:09+01:00 +Author: Katja Suess (ksuess) +Commit: https://github.com/plone/plone.app.discussion/commit/ee6b107a311f2c221b0680caaa348b1c41c7cbdb + +publish only pending comment, else show status message + +Files changed: +M plone/app/discussion/browser/moderation.py + +b'diff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py\nindex e05352e0..52c85a23 100644\n--- a/plone/app/discussion/browser/moderation.py\n+++ b/plone/app/discussion/browser/moderation.py\n@@ -8,6 +8,7 @@\n from plone.app.discussion.interfaces import _\n from plone.app.discussion.interfaces import IComment\n from plone.app.discussion.interfaces import IReplies\n+from plone.protect.interfaces import IDisableCSRFProtection\n from Products.CMFCore.utils import getToolByName\n from Products.Five.browser import BrowserView\n from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile\n@@ -206,6 +207,7 @@ class PublishComment(BrowserView):\n """\n \n def __call__(self):\n+ alsoProvides(self.request, IDisableCSRFProtection)\n comment = aq_inner(self.context)\n content_object = aq_parent(aq_parent(comment))\n workflowTool = getToolByName(comment, \'portal_workflow\', None)\n' + +Repository: plone.app.discussion + + +Branch: refs/heads/master +Date: 2020-01-08T12:22:09+01:00 +Author: Katja Suess (ksuess) +Commit: https://github.com/plone/plone.app.discussion/commit/840bc8daa639564d9a26570bef22dc2133001e21 + +changelog + +Files changed: +A news/163.enhancement + +b'diff --git a/news/163.enhancement b/news/163.enhancement\nnew file mode 100644\nindex 00000000..f6ee230f\n--- /dev/null\n+++ b/news/163.enhancement\n@@ -0,0 +1,2 @@\n+link of notification mail: /@@moderate-publish-comment : publish only pending comment, else show status message "comment already approved"\n+[ksuess]\n' + +Repository: plone.app.discussion + + +Branch: refs/heads/master +Date: 2020-01-08T12:22:09+01:00 +Author: Katja Suess (ksuess) +Commit: https://github.com/plone/plone.app.discussion/commit/875409daffa1cab1b08b9cd09e2b0f141882905b + +Moderation view tabbed + +two tabs for comments to moderate and comments approved + +Files changed: +M plone/app/discussion/browser/configure.zcml +M plone/app/discussion/browser/moderation.pt +M plone/app/discussion/browser/moderation.py +D plone/app/discussion/browser/comments_approved.pt + +b'diff --git a/plone/app/discussion/browser/comments_approved.pt b/plone/app/discussion/browser/comments_approved.pt\ndeleted file mode 100644\nindex 1149e97e..00000000\n--- a/plone/app/discussion/browser/comments_approved.pt\n+++ /dev/null\n@@ -1,129 +0,0 @@\n-\n-\n-\n-\n- \n- \n-\n-

\n- Comments approved\n-

\n-\n-
\n- Warning\n- \n- Moderation workflow is disabled. You have to\n- \n- enable the \'Comment Review Workflow\' for the Comment content\n- type before you can moderate comments here.\n- \n-
\n-\n-
\n-
\n-

\n- No comments approved\n-

\n-
\n-
\n-\n-
\n-\n-
\n-\n-
\n-\n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n-\n- \n- \n- \n- \n-
CommenterDateIn Response ToCommentApproved byAction
\n- Name\n- \n-
\n- Email\n- \n-
\n-
\n- \n- \n- \n- \n- show full comment text\n- \n- \n- last history entry\n- \n- \n- \n- \n-
\n-
\n-
\n-
\n-\n-
\n-
\n-\n-\n-\ndiff --git a/plone/app/discussion/browser/configure.zcml b/plone/app/discussion/browser/configure.zcml\nindex 6caff55b..7bb5aace 100644\n--- a/plone/app/discussion/browser/configure.zcml\n+++ b/plone/app/discussion/browser/configure.zcml\n@@ -29,23 +29,6 @@\n permission="plone.app.discussion.ReviewComments"\n />\n \n- \n- \n-\n- \n-\n \n \n Moderate comments\n \n-

\n- > Comments approved\n-

\n \n
\n
\n \n-
\n-
\n-

\n- No comments to moderate.\n-

\n+
\n+
\n+ \n+ Moderate comments\n+
\n+ \n+
\n+

\n+ No comments to moderate.\n+

\n+
\n+ \n+\n+
\n+\n+
\n+\n+
\n+\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+ \n+ \n+ \n+
CommenterDateIn Response ToCommentAction
\n+ \n+ \n+ \n+ Name\n+ \n+
\n+ Email\n+ \n+
\n+
\n+ \n+ \n+ \n+ \n+ show full comment text\n+ \n+ \n+ \n+
\n+
\n+
\n+
\n+
\n
\n- \n-\n-
\n-\n-
\n-\n-
\n-\n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n-
\n- \n- \n- \n-
CommenterDateIn Response ToCommentAction
\n- \n- \n- \n- Name\n- \n-
\n- Email\n- \n-
\n-
\n- \n- \n- \n- \n- show full comment text\n- \n- \n- \n-
\n-
\n+
\n+ \n+ Approved comments\n+
\n+ \n+
\n+

\n+ No comments approved\n+

\n+
\n+ \n+\n+
\n+\n+
\n+\n+
\n+\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+\n+ \n+ \n+ \n+ \n+
CommenterDateIn Response ToCommentApproved byAction
\n+ Name\n+ \n+
\n+ Email\n+ \n+
\n+
\n+ \n+ \n+ \n+ \n+ show full comment text\n+ \n+ \n+ last history entry\n+ \n+ \n+ \n+ \n+
\n+
\n+
\n+
\n+\n+
\n
\n- \n+
\n+\n+\n \n \n \ndiff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py\nindex 52c85a23..ac9af205 100644\n--- a/plone/app/discussion/browser/moderation.py\n+++ b/plone/app/discussion/browser/moderation.py\n@@ -35,6 +35,10 @@ def __call__(self):\n review_state=\'pending\',\n sort_on=\'created\',\n sort_order=\'reverse\')\n+ self.comments_approved = catalog(object_provides=IComment.__identifier__,\n+ review_state=\'published\',\n+ sort_on=\'created\',\n+ sort_order=\'reverse\')\n return self.template()\n \n def moderation_enabled(self):\n@@ -54,31 +58,6 @@ def moderation_enabled(self):\n return False\n \n \n-class ApprovedView(View):\n- """Overview comments already approved."""\n- template = ViewPageTemplateFile(\'comments_approved.pt\')\n- try:\n- template.id = \'@@comments-approved\'\n- except AttributeError:\n- # id is not writeable in Zope 2.12\n- pass\n-\n- def __call__(self):\n- self.request.set(\'disable_border\', True)\n- context = aq_inner(self.context)\n- catalog = getToolByName(context, \'portal_catalog\')\n- self.comments = catalog(object_provides=IComment.__identifier__,\n- review_state=\'published\',\n- sort_on=\'created\',\n- sort_order=\'reverse\')\n-\n- # print("*** approved comments")\n- # print(self.comments)\n- # for el in self.comments:\n- # print(el.id, el.review_state)\n- return self.template()\n-\n-\n class ModerateCommentsEnabled(BrowserView):\n \n def __call__(self):\n' + +Repository: plone.app.discussion + + +Branch: refs/heads/master +Date: 2020-01-08T12:22:09+01:00 +Author: Katja Suess (ksuess) +Commit: https://github.com/plone/plone.app.discussion/commit/f7b8335d272f5587237866eb53f10dd50be1240f + +additional workflow with rejected state + +moderation view and approved comments view: buttons for reject and approve + +Files changed: +A plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml +M plone/app/discussion/browser/configure.zcml +M plone/app/discussion/browser/javascripts/moderation.js +M plone/app/discussion/browser/moderation.pt +M plone/app/discussion/browser/moderation.py +M plone/app/discussion/profiles/default/workflows.xml + +b'diff --git a/plone/app/discussion/browser/configure.zcml b/plone/app/discussion/browser/configure.zcml\nindex 7bb5aace..c0890741 100644\n--- a/plone/app/discussion/browser/configure.zcml\n+++ b/plone/app/discussion/browser/configure.zcml\n@@ -100,6 +100,15 @@\n permission="plone.app.discussion.ReviewComments"\n />\n \n+ \n+ \n+\n \n tbody > tr");\n- if (comments.length === 1) {\n+ if (moderate) {\n+ // fade out row\n+ $(row).fadeOut("normal", function () {\n+ $(this).remove();\n+ });\n+ // reload page if all comments have been removed\n+ var comments = $("table#review-comments > tbody > tr");\n+ if (comments.length === 1) {\n+ location.reload();\n+ }\n+ } else {\n+ location.reload();\n+ }\n+ },\n+ error: function (msg) { // jshint ignore:line\n+ alert("Error sending AJAX request:" + target);\n+ }\n+ });\n+ });\n+\n+\n+ /**********************************************************************\n+ * Reject a single comment.\n+ **********************************************************************/\n+ $("input[name=\'form.button.Reject\']").click(function (e) {\n+ e.preventDefault();\n+ var row = $(this).parent().parent();\n+ var path = $(row).find("[name=\'selected_obj_paths:list\']").attr("value");\n+ var auth_key = $(\'input[name="_authenticator"]\').val();\n+ var target = path + "/@@moderate-reject-comment?_authenticator=" + auth_key;\n+ var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments";\n+ $.ajax({\n+ type: "GET",\n+ url: target,\n+ success: function (msg) { // jshint ignore:line\n+ if (moderate) {\n+ // fade out row\n+ $(row).fadeOut("normal", function () {\n+ $(this).remove();\n+ });\n+ // reload page if all comments have been removed\n+ var comments = $("table#review-comments > tbody > tr");\n+ if (comments.length === 1) {\n+ location.reload();\n+ }\n+ } else {\n location.reload();\n }\n },\n@@ -171,7 +211,8 @@ require([ // jshint ignore:line\n **********************************************************************/\n $(".last-history-entry").each(function() {\n $(this).load($(this).attr("data-href") + " .historyByLine", function() {\n- $(this).children(".historyByLine").last().remove();\n+ let currententry = $(this).children(".historyByLine").first();\n+ $(this).html(currententry);\n });\n });\n \ndiff --git a/plone/app/discussion/browser/moderation.pt b/plone/app/discussion/browser/moderation.pt\nindex 6f5aea0e..e3e56bb9 100644\n--- a/plone/app/discussion/browser/moderation.pt\n+++ b/plone/app/discussion/browser/moderation.pt\n@@ -142,7 +142,16 @@\n name="form.button.Publish"\n i18n:attributes="value label_publish;"\n tal:attributes="id item/id"\n- tal:condition="python:item.review_state == \'pending\'"\n+ tal:condition="python:item.review_state in [\'pending\',]"\n+ />\n+ \n \n
\n
\n-
\n+
\n

\n No comments approved\n

\n@@ -179,7 +188,7 @@\n tal:condition="items_approved_or_rejected"\n tal:define="batch python:Batch(items_approved_or_rejected, b_size, int(b_start), orphan=1);">\n \n-
\n+
\n \n
\n \n@@ -190,7 +199,7 @@\n Date\n In Response To\n Comment\n- Approved by\n+ Last Action\n Action\n \n \n@@ -215,8 +224,8 @@\n \n \n- \n- \n+ \n+ \n \n \n-\n \n \n+ \n+ \n \n \n \n+ \n \n \n \ndiff --git a/plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml b/plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml\nnew file mode 100644\nindex 00000000..14c53cca\n--- /dev/null\n+++ b/plone/app/discussion/profiles/default/workflows/comment_3state_review_workflow/definition.xml\n@@ -0,0 +1,105 @@\n+\n+\n+ Access contents information\n+ Modify portal content\n+ Reply to item\n+ View\n+ \n+ Submitted, pending review.\n+ \n+ \n+ \n+ Manager\n+ Owner\n+ Reviewer\n+ \n+ \n+ Manager\n+ Owner\n+ Reviewer\n+ \n+ \n+ \n+ \n+ Manager\n+ Owner\n+ Reviewer\n+ \n+ \n+ \n+ Visible to everyone, non-editable.\n+ \n+ \n+ \n+ \n+ Manager\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ Approving the comment makes it visible to other users.\n+ Approve\n+ \n+ Review comments\n+ \n+ \n+ \n+ Reject\n+ \n+ Review comments\n+ \n+ \n+ \n+ Previous transition\n+ \n+\n+ transition/getId|nothing\n+ \n+ \n+ \n+ \n+ \n+ The ID of the user who performed the previous transition\n+ \n+\n+ user/getUserName\n+ \n+ \n+ \n+ \n+ \n+ Comment about the last transition\n+ \n+\n+ python:state_change.kwargs.get(\'comment\', \'\')\n+ \n+ \n+ \n+ \n+ \n+ Provides access to workflow history\n+ \n+\n+ state_change/getHistory\n+ \n+ \n+ Request review\n+ Review portal content\n+ \n+ \n+ \n+ When the previous transition was performed\n+ \n+\n+ state_change/getDateTime\n+ \n+ \n+ \n+ \n+\n' + +Repository: plone.app.discussion + + +Branch: refs/heads/master +Date: 2020-01-08T12:36:28+01:00 +Author: Katja Suess (ksuess) +Commit: https://github.com/plone/plone.app.discussion/commit/084d2893e7222a984c31112283ddd9e671981fba + +Additional (optional) workflow: "Comment Multiple State Review Workflow" + +Moderator is not forced to delete a comment or to let it pending: +Workflow has two more states "rejected" and "spam" to existing review workflow. +Moderation view extended showing all states. Filter by state. + +Files changed: +A news/164.enhancement +A plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml +M plone/app/discussion/browser/comments.pt +M plone/app/discussion/browser/configure.zcml +M plone/app/discussion/browser/controlpanel.py +M plone/app/discussion/browser/javascripts/comments.js +M plone/app/discussion/browser/javascripts/moderation.js +M plone/app/discussion/browser/moderation.pt +M plone/app/discussion/browser/moderation.py +M plone/app/discussion/profiles/default/metadata.xml +M plone/app/discussion/profiles/default/workflows.xml +M plone/app/discussion/tests/functional_test_comment_review_workflow.txt +M plone/app/discussion/tests/functional_test_comments.txt +M plone/app/discussion/tests/test_moderation_view.py +M plone/app/discussion/tests/test_workflow.py +M plone/app/discussion/upgrades.py +M plone/app/discussion/upgrades.zcml + +b'diff --git a/news/164.enhancement b/news/164.enhancement\nnew file mode 100644\nindex 00000000..6feb63a9\n--- /dev/null\n+++ b/news/164.enhancement\n@@ -0,0 +1,3 @@\n+Additional optional workflow: workflow with two more states: rejected and spam,\n+ added to existing states pending and published. Moderation view extended to handle four workflow states.\n+[ksuess]\ndiff --git a/plone/app/discussion/browser/comments.pt b/plone/app/discussion/browser/comments.pt\nindex bf6671bc..1dd26b6f 100644\n--- a/plone/app/discussion/browser/comments.pt\n+++ b/plone/app/discussion/browser/comments.pt\n@@ -39,8 +39,9 @@\n portrait_url python:view.get_commenter_portrait(reply.author_username);\n review_state python:wtool.getInfoFor(reply, \'review_state\', \'none\');\n canEdit python:view.can_edit(reply);\n- canDelete python:view.can_delete(reply)"\n- tal:attributes="class python:\'comment replyTreeLevel\'+str(depth)+\' state-\'+str(review_state);\n+ canDelete python:view.can_delete(reply);\n+ colorclass python:lambda x: \'state-private\' if x==\'rejected\' else (\'state-internal\' if x==\'spam\' else \'state-\'+x);"\n+ tal:attributes="class python:\'comment replyTreeLevel{depth} {state}\'.format(depth= depth, state=colorclass(review_state));\n id string:${reply/getId}"\n tal:condition="python:canReview or review_state == \'published\'">\n \n@@ -119,7 +120,7 @@\n \n \n- Edit\n@@ -146,10 +147,10 @@\n class="commentactionsform"\n tal:condition="canReview"\n tal:repeat="action reply_dict/actions|nothing"\n- tal:attributes="action string:${reply/absolute_url}/@@moderate-publish-comment;\n+ tal:attributes="action string:${reply/absolute_url}/@@transmit-comment;\n name action/id">\n \n- \n \n- \n+ \n \n \n- \n- \n \n \n tbody > tr");\n- if (comments.length === 1) {\n- location.reload();\n- }\n- },\n- error: function (msg) { // jshint ignore:line\n- alert("Error sending AJAX request:" + target);\n- }\n- });\n- });\n-\n-\n- /**********************************************************************\n- * Publish a single comment.\n- **********************************************************************/\n- $("input[name=\'form.button.Publish\']").click(function (e) {\n- e.preventDefault();\n- var row = $(this).parent().parent();\n- var path = $(row).find("[name=\'selected_obj_paths:list\']").attr("value");\n- var auth_key = $(\'input[name="_authenticator"]\').val();\n- var target = path + "/@@moderate-publish-comment?_authenticator=" + auth_key;\n- var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments";\n- $.ajax({\n- type: "GET",\n- url: target,\n- success: function (msg) { // jshint ignore:line\n- if (moderate) {\n- // fade out row\n- $(row).fadeOut("normal", function () {\n- $(this).remove();\n- });\n- // reload page if all comments have been removed\n- var comments = $("table#review-comments > tbody > tr");\n- if (comments.length === 1) {\n- location.reload();\n- }\n- } else {\n- location.reload();\n- }\n- },\n- error: function (msg) { // jshint ignore:line\n- alert("Error sending AJAX request:" + target);\n- }\n- });\n- });\n-\n+require(["jquery", "pat-registry"], function($, registry) {\n+ "use strict";\n+\n+ $(document).ready(function() {\n+ init();\n+ });\n+\n+ function init() {\n+ /**********************************************************************\n+ * Delete a single comment.\n+ **********************************************************************/\n+ $("input[name=\'form.button.moderation.DeleteComment\']").click(function(e) {\n+ e.preventDefault();\n+ var row = $(this).closest("tr");\n+ var path = row.find("[name=\'selected_obj_paths:list\']").attr("value");\n+ var auth_key = $(\'input[name="_authenticator"]\').val();\n+ var target =\n+ path + "/@@moderate-delete-comment?_authenticator=" + auth_key;\n+ $.ajax({\n+ type: "GET",\n+ url: target,\n+ success: function(msg) {\n+ // fade out row\n+ row.fadeOut(250).fadeIn(250, function() {\n+ row.remove();\n+ });\n+ // reload page if all comments have been removed\n+ var comments = $("table#review-comments > tbody > tr");\n+ if (comments.length === 1) {\n+ location.reload();\n+ }\n+ },\n+ error: function(msg) {\n+ alert("Error sending AJAX request:" + target);\n+ }\n+ });\n+ });\n \n- /**********************************************************************\n- * Reject a single comment.\n- **********************************************************************/\n- $("input[name=\'form.button.Reject\']").click(function (e) {\n- e.preventDefault();\n- var row = $(this).parent().parent();\n- var path = $(row).find("[name=\'selected_obj_paths:list\']").attr("value");\n- var auth_key = $(\'input[name="_authenticator"]\').val();\n- var target = path + "/@@moderate-reject-comment?_authenticator=" + auth_key;\n- var moderate = $(this).closest("fieldset").attr("id") == "fieldset-moderate-comments";\n- $.ajax({\n- type: "GET",\n- url: target,\n- success: function (msg) { // jshint ignore:line\n- if (moderate) {\n- // fade out row\n- $(row).fadeOut("normal", function () {\n- $(this).remove();\n- });\n- // reload page if all comments have been removed\n- var comments = $("table#review-comments > tbody > tr");\n- if (comments.length === 1) {\n- location.reload();\n- }\n- } else {\n- location.reload();\n- }\n- },\n- error: function (msg) { // jshint ignore:line\n- alert("Error sending AJAX request:" + target);\n- }\n+ /**********************************************************************\n+ * Transmit a single comment.\n+ **********************************************************************/\n+ $(\'input[name="form.button.moderation.TransmitComment"]\').click(function(\n+ e\n+ ) {\n+ e.preventDefault();\n+ let button = $(this);\n+ var row = $(this).closest("tr");\n+ var path = $(row)\n+ .find("[name=\'selected_obj_paths:list\']")\n+ .attr("value");\n+ var workflow_action = $(this).attr("data-transition");\n+ var auth_key = $(\'input[name="_authenticator"]\').val();\n+ // distinction of workflow_action\n+ var target =\n+ path +\n+ "/@@transmit-comment?_authenticator=" +\n+ auth_key +\n+ "&workflow_action=" +\n+ workflow_action;\n+ var moderate =\n+ $(this)\n+ .closest("fieldset")\n+ .attr("id") == "fieldset-moderate-comments";\n+ $.ajax({\n+ type: "GET",\n+ url: target,\n+ success: function(msg) {\n+ if (moderate) {\n+ let url = location.href;\n+ $("#review-comments").load(url + " #review-comments", function() {\n+ init();\n+ $(".pat-plone-modal").patPloneModal();\n });\n- });\n-\n-\n- /**********************************************************************\n- * Bulk actions for comments (delete, publish)\n- **********************************************************************/\n- $("input[name=\'form.button.BulkAction\']").click(function (e) {\n- e.preventDefault();\n- var form = $(this).parents("form");\n- var target = $(form).attr(\'action\');\n- var params = $(form).serialize();\n- var valArray = $(\'input:checkbox:checked\');\n- var selectField = $(form).find("[name=\'form.select.BulkAction\']");\n- if (selectField.val() === \'-1\') {\n- // XXX: translate message\n- alert("You haven\'t selected a bulk action. Please select one.");\n- } else if (valArray.length === 0) {\n- // XXX: translate message\n- alert("You haven\'t selected any comment for this bulk action." +\n- "Please select at least one comment.");\n- } else {\n- $.post(target, params, function (data) { // jshint ignore:line\n- valArray.each(function () {\n- /* Remove all selected lines. */\n- var row = $(this).parent().parent();\n- row.fadeOut("normal", function () {\n- row.remove();\n- });\n- });\n- // reload page if all comments have been removed\n- var comments = $("table#review-comments > tbody > tr");\n- if (comments.length <= valArray.length) {\n- location.reload();\n- }\n- });\n- // reset the bulkaction select\n- selectField.find("option[value=\'-1\']").attr(\'selected\', \'selected\');\n- }\n- });\n-\n-\n- /**********************************************************************\n- * Check or uncheck all checkboxes from the batch moderation page.\n- **********************************************************************/\n- $("input[name=\'check_all\']").click(function () {\n- if ($(this).val() === \'0\') {\n- $(this).parents("table")\n- .find("input:checkbox")\n- .attr("checked", "checked");\n- $(this).val("1");\n- } else {\n- $(this).parents("table")\n- .find("input:checkbox")\n- .attr("checked", "");\n- $(this).val("0");\n- }\n- });\n-\n+ } else {\n+ location.reload();\n+ }\n+ },\n+ error: function(msg) {\n+ alert(\n+ "Error transmitting comment. (Error sending AJAX request:" +\n+ target +\n+ ")"\n+ );\n+ }\n+ });\n+ });\n \n- /**********************************************************************\n- * Show full text of a comment in the batch moderation page.\n- **********************************************************************/\n- $(".show-full-comment-text").click(function (e) {\n- e.preventDefault();\n- var target = $(this).attr("href");\n- var td = $(this).parent();\n- $.ajax({\n- type: "GET",\n- url: target,\n- data: "",\n- success: function (data) {\n- // show full text\n- td.replaceWith("" + data + "");\n- },\n- error: function (msg) { // jshint ignore:line\n- alert("Error getting full comment text:" + target);\n- }\n+ /**********************************************************************\n+ * Bulk actions for comments (delete, publish)\n+ **********************************************************************/\n+ $("input[name=\'form.button.BulkAction\']").click(function(e) {\n+ e.preventDefault();\n+ var form = $(this).parents("form");\n+ var target = $(form).attr("action");\n+ var params = $(form).serialize();\n+ var valArray = $("input:checkbox:checked");\n+ var selectField = $(form).find("[name=\'form.select.BulkAction\']");\n+ if (selectField.val() === "-1") {\n+ // XXX: translate message\n+ alert("You haven\'t selected a bulk action. Please select one.");\n+ } else if (valArray.length === 0) {\n+ // XXX: translate message\n+ alert(\n+ "You haven\'t selected any comment for this bulk action." +\n+ "Please select at least one comment."\n+ );\n+ } else {\n+ $.post(target, params, function(data) {\n+ valArray.each(function() {\n+ /* Remove all selected lines. */\n+ var row = $(this)\n+ .parent()\n+ .parent();\n+ row.fadeOut("normal", function() {\n+ row.remove();\n });\n+ });\n+ // reload page if all comments have been removed\n+ var comments = $("table#review-comments > tbody > tr");\n+ if (comments.length <= valArray.length) {\n+ location.reload();\n+ }\n });\n+ // reset the bulkaction select\n+ selectField.find("option[value=\'-1\']").attr("selected", "selected");\n+ }\n+ });\n \n+ /**********************************************************************\n+ * Check or uncheck all checkboxes from the batch moderation page.\n+ **********************************************************************/\n+ $("input[name=\'check_all\']").click(function() {\n+ if ($(this).val() === "0") {\n+ $(this)\n+ .parents("table")\n+ .find("input:checkbox")\n+ .prop("checked", true);\n+ $(this).val("1");\n+ } else {\n+ $(this)\n+ .parents("table")\n+ .find("input:checkbox")\n+ .prop("checked", false);\n+ $(this).val("0");\n+ }\n+ });\n \n- /**********************************************************************\n- * Comments approved: Load history for approved date.\n- **********************************************************************/\n- $(".last-history-entry").each(function() {\n- $(this).load($(this).attr("data-href") + " .historyByLine", function() {\n- let currententry = $(this).children(".historyByLine").first();\n- $(this).html(currententry);\n- });\n- });\n-\n+ /**********************************************************************\n+ * select comments with review_state\n+ **********************************************************************/\n+\n+ $("input[name=\'review_state\']").click(function() {\n+ // location.search = \'review_state=\' + $(this).val();\n+ let review_state = $(this).val();\n+ let url = location.href;\n+ if (location.search) {\n+ url = location.href.replace(\n+ location.search,\n+ "?review_state=" + review_state\n+ );\n+ } else {\n+ url = location.href + "?review_state=" + review_state;\n+ }\n+\n+ $("#review-comments").load(url + " #review-comments", function() {\n+ init();\n+ $(".pat-plone-modal").patPloneModal();\n+ let stateObj = { review_state: review_state };\n+ history.pushState(stateObj, "moderate comments", url);\n+ });\n });\n \n- //#JSCOVERAGE_ENDIF\n+ /**********************************************************************\n+ * Show full text of a comment in the batch moderation page.\n+ **********************************************************************/\n+ $(".show-full-comment-text").click(function(e) {\n+ e.preventDefault();\n+ var target = $(this).attr("href");\n+ var parent = $(this).parent();\n+ $.ajax({\n+ type: "GET",\n+ url: target,\n+ data: "",\n+ success: function(data) {\n+ // show full text\n+ parent.html(data);\n+ },\n+ error: function(msg) {\n+ alert("Error getting full comment text:" + target);\n+ }\n+ });\n+ });\n \n+ /**********************************************************************\n+ * Comments approved: Load history for approved date.\n+ **********************************************************************/\n+ $(".last-history-entry").each(function() {\n+ var me = $(this);\n+ $.ajax({\n+ url: me.attr("data-href"),\n+ success: function(data) {\n+ let first_history_entry = $(data)\n+ .find(".historyByLine")\n+ .first();\n+ me.html("");\n+ first_history_entry.children().each(function() {\n+ me.append($(this));\n+ me.append("
");\n+ });\n+ // format date\n+ registry.scan(me);\n+ },\n+ error: function(msg) {\n+ alert("Error getting history.");\n+ }\n+ });\n+ });\n+ } // end init\n });\ndiff --git a/plone/app/discussion/browser/moderation.pt b/plone/app/discussion/browser/moderation.pt\nindex e3e56bb9..fb99297c 100644\n--- a/plone/app/discussion/browser/moderation.pt\n+++ b/plone/app/discussion/browser/moderation.pt\n@@ -17,7 +17,9 @@\n b_size python:30;\n b_start python:0;\n b_start request/b_start | b_start;\n- moderation_enabled view/moderation_enabled;">\n+ moderation_enabled view/moderation_enabled;\n+ colorclass python:lambda x: \'state-private\' if x==\'rejected\' else (\'state-internal\' if x==\'spam\' else \'state-\'+x);\n+ ">\n \n@@ -40,245 +42,163 @@\n \n \n \n-
\n-
\n- \n- Moderate comments\n-
\n- \n-
\n-

\n- No comments to moderate.\n-

\n-
\n- \n \n-
\n-\n-
\n-\n-
\n-\n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n-
\n- \n- \n- \n-
CommenterDateIn Response ToCommentAction
\n- \n- \n- \n- Name\n- \n-
\n- Email\n- \n-
\n-
\n- \n- \n- \n- \n- show full comment text\n- \n- \n- \n- \n-
\n-
\n-
\n-
\n-
\n-
\n-
\n- \n- Approved comments\n-
\n-
\n-
\n-

\n- No comments approved\n-

\n-
\n-
\n-\n-
\n-\n-
\n-\n-
\n-\n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n- \n-
CommenterDateIn Response ToCommentLast ActionAction
\n- Name\n- \n-
\n- Email\n- \n-
\n-
\n- \n- \n- \n- \n- show full comment text\n- \n- \n- last history entry\n- \n- \n- \n- \n- \n- \n-
\n-
\n-
\n-
\n-\n-
\n-
\n-
\n+
\n+
\n+

\n+ No comments\n+

\n+
\n+
\n+\n+
\n+\n+
\n+\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+ \n+ \n+
CommenterDateIn Response ToCommentLast Action
\n+ \n+ \n+ \n+ Name\n+ \n+
\n+ Email\n+ \n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+ \n+ \n+ \n+ \n+ Edit\n+\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+ last history entry\n+ \n+
\n+
\n+
\n+
\n \n \n \ndiff --git a/plone/app/discussion/browser/moderation.py b/plone/app/discussion/browser/moderation.py\nindex 977b08ab..2ed64d7e 100644\n--- a/plone/app/discussion/browser/moderation.py\n+++ b/plone/app/discussion/browser/moderation.py\n@@ -1,4 +1,4 @@\n-# -*- coding: utf-8 -*-\n+# coding: utf-8\n from AccessControl import getSecurityManager\n from AccessControl import Unauthorized\n from Acquisition import aq_inner\n@@ -17,9 +17,23 @@\n from zope.interface import alsoProvides\n \n \n+# Translations for generated values in buttons\n+# States\n+_(\'comment_pending\', default=\'pending\')\n+# _(\'comment_approved\', default=\'approved\')\n+_(\'comment_published\', default=\'approved\')\n+_(\'comment_rejected\', default=\'rejected\')\n+_(\'comment_spam\', default=\'marked as spam\')\n+# Transitions\n+_(\'Recall\')\n+_(\'Approve\')\n+_(\'Reject\')\n+_(\'Spam\')\n+\n+\n class View(BrowserView):\n- """Comment moderation view.\n- """\n+ """Show comment moderation view."""\n+\n template = ViewPageTemplateFile(\'moderation.pt\')\n try:\n template.id = \'@@moderate-comments\'\n@@ -27,53 +41,91 @@ class View(BrowserView):\n # id is not writeable in Zope 2.12\n pass\n \n+ def __init__(self, context, request):\n+ self.context = context\n+ self.request = request\n+ self.workflowTool = getToolByName(self.context, \'portal_workflow\')\n+\n def __call__(self):\n self.request.set(\'disable_border\', True)\n- context = aq_inner(self.context)\n- catalog = getToolByName(context, \'portal_catalog\')\n- self.comments = catalog(object_provides=IComment.__identifier__,\n- review_state=\'pending\',\n- sort_on=\'created\',\n- sort_order=\'reverse\')\n- self.comments_approved = catalog(object_provides=IComment.__identifier__,\n- review_state=[\'published\', \'rejected\'],\n- sort_on=\'created\',\n- sort_order=\'reverse\')\n+ self.request.set(\'review_state\',\n+ self.request.get(\'review_state\', \'pending\'))\n return self.template()\n \n+ def comments(self):\n+ """Return comments of defined review_state.\n+\n+ review_state is string or list of strings.\n+ """\n+ catalog = getToolByName(self.context, \'portal_catalog\')\n+ if self.request.review_state == \'all\':\n+ return catalog(object_provides=IComment.__identifier__,\n+ sort_on=\'created\',\n+ sort_order=\'reverse\')\n+ return catalog(object_provides=IComment.__identifier__,\n+ review_state=self.request.review_state,\n+ sort_on=\'created\',\n+ sort_order=\'reverse\')\n+\n def moderation_enabled(self):\n- """Returns true if a \'review workflow\' is enabled on \'Discussion Item\'\n- content type. A \'review workflow\' is characterized by implementing\n- a \'pending\' workflow state.\n+ """Return true if a review workflow is enabled on \'Discussion Item\'\n+ content type.\n+\n+ A \'review workflow\' is characterized by implementing a \'pending\'\n+ workflow state.\n """\n- context = aq_inner(self.context)\n- workflowTool = getToolByName(context, \'portal_workflow\')\n- comment_workflow = workflowTool.getChainForPortalType(\n+ comment_workflow = self.workflowTool.getChainForPortalType(\n \'Discussion Item\')\n if comment_workflow:\n comment_workflow = comment_workflow[0]\n- comment_workflow = workflowTool[comment_workflow]\n+ comment_workflow = self.workflowTool[comment_workflow]\n if \'pending\' in comment_workflow.states:\n return True\n return False\n \n @property\n- def moderation_3state(self):\n- """Returns true if a \'review 3 state workflow\' is enabled on \'Discussion Item\'\n- content type. A \'review 3 state workflow\' is characterized by implementing\n- a \'rejected\' workflow state.\n+ def moderation_multiple_state_enabled(self):\n+ """Return true if a \'review multiple state workflow\' is enabled on\n+ \'Discussion Item\' content type.\n+\n+ A \'review multipe state workflow\' is characterized by implementing\n+ a \'rejected\' workflow state and a \'spam\' workflow state.\n """\n- context = aq_inner(self.context)\n- workflowTool = getToolByName(context, \'portal_workflow\')\n- comment_workflow = workflowTool.getChainForPortalType(\n+ comment_workflow = self.workflowTool.getChainForPortalType(\n \'Discussion Item\')\n if comment_workflow:\n comment_workflow = comment_workflow[0]\n- comment_workflow = workflowTool[comment_workflow]\n+ comment_workflow = self.workflowTool[comment_workflow]\n if \'rejected\' in comment_workflow.states:\n return True\n return False\n \n+ def allowed_transitions(self, obj=None):\n+ """Return allowed workflow transitions.\n+\n+ Example: pending\n+\n+ [{\'id\': \'mark_as_spam\', \'url\': \'http://localhost:8083/PloneRejected/testfolder/testpage/++conversation++default/1575415863542780/content_status_modify?workflow_action=mark_as_spam\', \'icon\': \'\', \'category\': \'workflow\', \'transition\': , \'title\': \'Spam\', \'link_target\': None, \'visible\': True, \'available\': True, \'allowed\': True},\n+ {\'id\': \'publish\',\n+ \'url\': \'http://localhost:8083/PloneRejected/testfolder/testpage/++conversation++default/1575415863542780/content_status_modify?workflow_action=publish\',\n+ \'icon\': \'\',\n+ \'category\': \'workflow\',\n+ \'transition\': ,\n+ \'title\': \'Approve\',\n+ \'link_target\': None, \'visible\': True, \'available\': True, \'allowed\': True},\n+ {\'id\': \'reject\', \'url\': \'http://localhost:8083/PloneRejected/testfolder/testpage/++conversation++default/1575415863542780/content_status_modify?workflow_action=reject\', \'icon\': \'\', \'category\': \'workflow\', \'transition\': , \'title\': \'Reject\', \'link_target\': None, \'visible\': True, \'available\': True, \'allowed\': True}]\n+ """\n+\n+ if obj:\n+ transitions = [\n+ a for a in self.workflowTool.listActionInfos(object=obj)\n+ if a[\'category\'] == \'workflow\' and a[\'allowed\']\n+ ]\n+ return transitions\n+\n+ def translate(self, text=""):\n+ return _(text)\n+\n \n class ModerateCommentsEnabled(BrowserView):\n \n@@ -181,13 +233,13 @@ def __call__(self):\n raise Unauthorized("You\'re not allowed to delete this comment.")\n \n \n-class PublishComment(BrowserView):\n- """Publish a comment.\n+class CommentTransition(BrowserView):\n+ r"""Publish, reject, recall a comment or mark it as spam.\n \n This view is always called directly on the comment object:\n \n http://nohost/front-page/++conversation++default/1286289644723317/\\\n- @@moderate-publish-comment\n+ @@transmit-comment\n \n Each table row (comment) in the moderation view contains a hidden input\n field with the absolute URL of the content object:\n@@ -203,31 +255,39 @@ class PublishComment(BrowserView):\n """\n \n def __call__(self):\n- alsoProvides(self.request, IDisableCSRFProtection)\n+ """Call CommentTransition."""\n comment = aq_inner(self.context)\n content_object = aq_parent(aq_parent(comment))\n print("*** called: PublishComment for ", comment.Description)\n workflowTool = getToolByName(comment, \'portal_workflow\', None)\n workflow_action = self.request.form.get(\'workflow_action\', \'publish\')\n review_state = workflowTool.getInfoFor(comment, \'review_state\', \'\')\n- if review_state != "published":\n- workflowTool.doActionFor(comment, workflow_action)\n- comment.reindexObject()\n- content_object.reindexObject(idxs=[\'total_comments\'])\n- notify(CommentPublishedEvent(self.context, comment))\n- IStatusMessage(self.context.REQUEST).addStatusMessage(\n- _(\'Comment approved.\'),\n- type=\'info\')\n- else:\n- IStatusMessage(self.context.REQUEST).addStatusMessage(\n- _(\'Comment already approved.\'),\n- type=\'info\')\n+ workflowTool.doActionFor(comment, workflow_action)\n+ comment.reindexObject()\n+ content_object.reindexObject(idxs=[\'total_comments\'])\n+ notify(CommentPublishedEvent(self.context, comment))\n+ review_state_new = workflowTool.getInfoFor(comment, \'review_state\', \'\')\n+\n+ # context.translate() does not know a default for untranslated msgids\n+ comment_state_translated = \\\n+ self.context.translate("comment_"+review_state_new)\n+ if comment_state_translated == "comment_"+review_state_new:\n+ comment_state_translated = review_state_new\n+\n+ msgid = _(\n+ "comment_transmitted",\n+ default=\'Comment ${comment_state_translated}.\',\n+ mapping={"comment_state_translated": comment_state_translated})\n+ translated = self.context.translate(msgid)\n+ IStatusMessage(self.context.REQUEST).addStatusMessage(\n+ translated, type=\'info\')\n+\n came_from = self.context.REQUEST.HTTP_REFERER\n # if the referrer already has a came_from in it, don\'t redirect back\n- if (len(came_from) == 0 or \'came_from=\' in came_from or\n- not getToolByName(\n- content_object, \'portal_url\').isURLInPortal(came_from) or\n- \'@@confirm-action\' in came_from):\n+ if (len(came_from) == 0\n+ or \'came_from=\' in came_from\n+ or not getToolByName(\n+ content_object, \'portal_url\').isURLInPortal(came_from)):\n came_from = content_object.absolute_url()\n return self.context.REQUEST.RESPONSE.redirect(came_from)\n \n@@ -268,7 +328,7 @@ def __call__(self):\n \n \n class BulkActionsView(BrowserView):\n- """Bulk actions (unapprove, approve, delete, mark as spam).\n+ """Bulk actions (approve, delete, reject, recall, mark as spam).\n \n Each table row of the moderation view has a checkbox with the absolute\n path (without host and port) of the comment objects:\n@@ -291,6 +351,7 @@ class BulkActionsView(BrowserView):\n """\n \n def __call__(self):\n+ """Call BulkActionsView."""\n if \'form.select.BulkAction\' in self.request:\n bulkaction = self.request.get(\'form.select.BulkAction\')\n self.paths = self.request.get(\'paths\')\ndiff --git a/plone/app/discussion/profiles/default/metadata.xml b/plone/app/discussion/profiles/default/metadata.xml\nindex c779e86e..49f2d5ec 100644\n--- a/plone/app/discussion/profiles/default/metadata.xml\n+++ b/plone/app/discussion/profiles/default/metadata.xml\n@@ -1,5 +1,5 @@\n \n- 1001\n+ 1002\n \n profile-plone.resource:default\n profile-plone.app.registry:default\ndiff --git a/plone/app/discussion/profiles/default/workflows.xml b/plone/app/discussion/profiles/default/workflows.xml\nindex 536fb374..820f22db 100644\n--- a/plone/app/discussion/profiles/default/workflows.xml\n+++ b/plone/app/discussion/profiles/default/workflows.xml\n@@ -1,7 +1,7 @@\n \n \n \n- \n+ \n \n \n \ndiff --git a/plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml b/plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml\nnew file mode 100644\nindex 00000000..c7759608\n--- /dev/null\n+++ b/plone/app/discussion/profiles/default/workflows/comment_multiple_state_review_workflow/definition.xml\n@@ -0,0 +1,138 @@\n+\n+\n+ Access contents information\n+ Modify portal content\n+ Reply to item\n+ View\n+ \n+ Submitted, pending review.\n+ \n+ \n+ \n+ \n+ Manager\n+ Owner\n+ Reviewer\n+ \n+ \n+ Manager\n+ Owner\n+ Reviewer\n+ \n+ \n+ \n+ \n+ Manager\n+ Owner\n+ Reviewer\n+ \n+ \n+ \n+ Visible to everyone, non-editable.\n+ \n+ \n+ \n+ \n+ \n+ \n+ Manager\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ Spam comments are invisible to other users.\n+ Spam\n+ \n+ Review comments\n+ \n+ \n+ \n+ Approving the comment makes it visible to other users.\n+ Approve\n+ \n+ Review comments\n+ \n+ \n+ \n+ Reject\n+ \n+ Review comments\n+ \n+ \n+ \n+ Rejected comments are invisible to other users.\n+ Reject\n+ \n+ Review comments\n+ \n+ \n+ \n+ Previous transition\n+ \n+\n+ transition/getId|nothing\n+ \n+ \n+ \n+ \n+ \n+ The ID of the user who performed the previous transition\n+ \n+\n+ user/getUserName\n+ \n+ \n+ \n+ \n+ \n+ Comment about the last transition\n+ \n+\n+ python:state_change.kwargs.get(\'comment\', \'\')\n+ \n+ \n+ \n+ \n+ \n+ Provides access to workflow history\n+ \n+\n+ state_change/getHistory\n+ \n+ \n+ Request review\n+ Review portal content\n+ \n+ \n+ \n+ When the previous transition was performed\n+ \n+\n+ state_change/getDateTime\n+ \n+ \n+ \n+ \n+\ndiff --git a/plone/app/discussion/tests/functional_test_comment_review_workflow.txt b/plone/app/discussion/tests/functional_test_comment_review_workflow.txt\nindex a835f964..55318883 100644\n--- a/plone/app/discussion/tests/functional_test_comment_review_workflow.txt\n+++ b/plone/app/discussion/tests/functional_test_comment_review_workflow.txt\n@@ -112,7 +112,7 @@ Administrators can see all posts and comment actions\n >>> \'form.button.DeleteComment\' in browser.contents\n True\n \n- >>> \'form.button.PublishComment\' in browser.contents\n+ >>> \'form.button.TransmitComment\' in browser.contents\n True\n \n Anonymous user can not see any posts or comment actions\n@@ -128,7 +128,7 @@ Anonymous user can not see any posts or comment actions\n >>> \'form.button.DeleteComment\' in unprivileged_browser.contents\n False\n \n- >>> \'form.button.PublishComment\' in unprivileged_browser.contents\n+ >>> \'form.button.TransmitComment\' in unprivileged_browser.contents\n False\n \n The catalog does not list the comments yet:\n@@ -156,7 +156,7 @@ actions.\n >>> \'form.button.DeleteComment\' in browser.contents\n True\n \n- >>> \'form.button.PublishComment\' in browser.contents\n+ >>> \'form.button.TransmitComment\' in browser.contents\n True\n \n \ndiff --git a/plone/app/discussion/tests/functional_test_comments.txt b/plone/app/discussion/tests/functional_test_comments.txt\nindex 36ad8a17..89bdeccf 100644\n--- a/plone/app/discussion/tests/functional_test_comments.txt\n+++ b/plone/app/discussion/tests/functional_test_comments.txt\n@@ -326,19 +326,19 @@ Deleting existing comments | \'Delete comments\' permission\n Anonymous cannot delete comments\n \n >>> unprivileged_browser.open(urldoc1)\n- >>> \'form.button.Delete\' in unprivileged_browser.contents\n+ >>> \'form.button.DeleteComment\' in unprivileged_browser.contents\n False\n \n A member cannot delete his own comments if he can\'t review or he isn\'t a Site Administrator\n \n >>> browser_member.open(urldoc1)\n- >>> \'form.button.Delete\' in browser_member.contents\n+ >>> \'form.button.DeleteComment\' in browser_member.contents\n False\n \n Admin can delete comments\n \n >>> browser.open(urldoc1)\n- >>> \'form.button.Delete\' in browser.contents\n+ >>> \'form.button.DeleteComment\' in browser.contents\n True\n \n Extract the delete comment url from the first "delete comment" button\ndiff --git a/plone/app/discussion/tests/test_moderation_view.py b/plone/app/discussion/tests/test_moderation_view.py\nindex 7e660c10..08d6cbd6 100644\n--- a/plone/app/discussion/tests/test_moderation_view.py\n+++ b/plone/app/discussion/tests/test_moderation_view.py\n@@ -1,7 +1,7 @@\n # -*- coding: utf-8 -*-\n from plone.app.discussion.browser.moderation import BulkActionsView\n from plone.app.discussion.browser.moderation import DeleteComment\n-from plone.app.discussion.browser.moderation import PublishComment\n+from plone.app.discussion.browser.moderation import CommentTransition\n from plone.app.discussion.browser.moderation import View\n from plone.app.discussion.interfaces import IConversation\n from plone.app.discussion.interfaces import IDiscussionSettings\n@@ -195,14 +195,14 @@ def setUp(self):\n def test_regression(self):\n page_url = self.page.absolute_url()\n self.request[\'HTTP_REFERER\'] = page_url\n- for Klass in (DeleteComment, PublishComment):\n+ for Klass in (DeleteComment, CommentTransition):\n view = Klass(self.comment, self.request)\n view.__parent__ = self.comment\n self.assertEqual(page_url, view())\n \n def test_valid_next_url(self):\n self.request[\'HTTP_REFERER\'] = \'http://attacker.com\'\n- for Klass in (DeleteComment, PublishComment):\n+ for Klass in (DeleteComment, CommentTransition):\n view = Klass(self.comment, self.request)\n view.__parent__ = self.comment\n self.assertNotEqual(\'http://attacker.com\', view())\ndiff --git a/plone/app/discussion/tests/test_workflow.py b/plone/app/discussion/tests/test_workflow.py\nindex aafe2222..9f959fcc 100644\n--- a/plone/app/discussion/tests/test_workflow.py\n+++ b/plone/app/discussion/tests/test_workflow.py\n@@ -272,7 +272,7 @@ def test_publish(self):\n \'review_state\',\n ),\n )\n- view = self.comment.restrictedTraverse(\'@@moderate-publish-comment\')\n+ view = self.comment.restrictedTraverse(\'@@transmit-comment\')\n view()\n self.assertEqual(\n \'published\',\n@@ -295,7 +295,7 @@ def test_publish_as_anonymous(self):\n self.assertRaises(\n Unauthorized,\n self.comment.restrictedTraverse,\n- \'@@moderate-publish-comment\',\n+ \'@@transmit-comment\',\n )\n self.assertEqual(\n \'pending\',\ndiff --git a/plone/app/discussion/upgrades.py b/plone/app/discussion/upgrades.py\nindex 7a0e30c1..2bb3c287 100644\n--- a/plone/app/discussion/upgrades.py\n+++ b/plone/app/discussion/upgrades.py\n@@ -20,7 +20,7 @@ def update_rolemap(context):\n context.runImportStepFromProfile(default_profile, \'rolemap\')\n \n \n-def upgrade_comment_workflows(context):\n+def upgrade_comment_workflows_retain_current_workflow(context):\n # If the current comment workflow is the one_state_workflow, running our\n # import step will change it to comment_one_state_workflow. This is good.\n # If it was anything else, we should restore this. So get the original\n@@ -46,13 +46,16 @@ def upgrade_comment_workflows(context):\n orig_chain[idx] = \'comment_one_state_workflow\'\n # Restore the chain.\n wf_tool.setChainForPortalTypes([portal_type], orig_chain)\n- new_chain = list(wf_tool.getChainFor(portal_type))\n- workflows = [wf_tool.getWorkflowById(wf_id)\n- for wf_id in new_chain]\n \n+\n+def upgrade_comment_workflows_apply_rolemapping(context):\n # Now go over the comments, update their role mappings, and reindex the\n # allowedRolesAndUsers index.\n+ portal_type = \'Discussion Item\'\n catalog = getToolByName(context, \'portal_catalog\')\n+ wf_tool = getToolByName(context, \'portal_workflow\')\n+ new_chain = list(wf_tool.getChainFor(portal_type))\n+ workflows = [wf_tool.getWorkflowById(wf_id) for wf_id in new_chain]\n for brain in catalog.unrestrictedSearchResults(portal_type=portal_type):\n try:\n comment = brain.getObject()\n@@ -63,5 +66,14 @@ def upgrade_comment_workflows(context):\n logger.info(\'Could not reindex comment {0}\'.format(brain.getURL()))\n \n \n+def upgrade_comment_workflows(context):\n+ upgrade_comment_workflows_retain_current_workflow(context)\n+ upgrade_comment_workflows_apply_rolemapping(context)\n+\n+\n def add_js_to_plone_legacy(context):\n context.runImportStepFromProfile(default_profile, \'plone.app.registry\')\n+\n+\n+def add_multiple_state_workflow(context):\n+ upgrade_comment_workflows_retain_current_workflow(context)\ndiff --git a/plone/app/discussion/upgrades.zcml b/plone/app/discussion/upgrades.zcml\nindex 02a7e541..d64591ce 100644\n--- a/plone/app/discussion/upgrades.zcml\n+++ b/plone/app/discussion/upgrades.zcml\n@@ -62,4 +62,15 @@\n />\n \n \n+ \n+ \n+ \n+\n \n' + +Repository: plone.app.discussion + + +Branch: refs/heads/master +Date: 2020-01-08T12:37:34+01:00 +Author: Katja Suess (ksuess) +Commit: https://github.com/plone/plone.app.discussion/commit/64d50fbd00b80a5bb5d108b8a63ab9d1b0b47ed2 + +fix docstring + +Files changed: +M plone/app/discussion/browser/moderation.pt +M plone/app/discussion/browser/moderation.py + +b'diff --git a/plone/app/discussion/browser/moderation.pt b/plone/app/discussion/browser/moderation.pt\nindex fb99297c..5b8b64c7 100644\n--- a/plone/app/discussion/browser/moderation.pt\n+++ b/plone/app/discussion/browser/moderation.pt\n@@ -11,7 +11,6 @@\n