From dc008a9e4a4e0536b0f849e0d742f0e01a81ad7b Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Thu, 8 Feb 2024 21:47:09 +0100 Subject: [PATCH 1/6] some refactoring to improve tests and allow users to access data --- .travis.yml | 61 ----- CHANGES.rst | 11 +- setup.py | 22 +- src/collective/feedback/configure.zcml | 2 + src/collective/feedback/permissions.zcml | 6 +- .../feedback/profiles/default/metadata.xml | 3 +- .../feedback/profiles/default/rolemap.xml | 4 + .../feedback/restapi/services/configure.zcml | 4 +- .../feedback/restapi/services/get.py | 71 +++--- src/collective/feedback/testing.py | 42 ++-- .../feedback/tests/robot/test_example.robot | 66 ------ .../feedback/tests/test_delete_content.py | 100 +++++++++ .../feedback/tests/test_feedbacks_add.py | 115 ++++++++++ .../feedback/tests/test_feedbacks_get.py | 211 ++++++++++++++++++ src/collective/feedback/tests/test_robot.py | 33 --- src/collective/feedback/tests/test_setup.py | 67 ------ src/collective/feedback/tests/test_store.py | 4 +- src/collective/feedback/upgrades.py | 53 +++++ src/collective/feedback/upgrades.zcml | 20 ++ test_plone60.cfg | 35 +++ 20 files changed, 630 insertions(+), 300 deletions(-) delete mode 100644 .travis.yml delete mode 100644 src/collective/feedback/tests/robot/test_example.robot create mode 100644 src/collective/feedback/tests/test_delete_content.py create mode 100644 src/collective/feedback/tests/test_feedbacks_add.py create mode 100644 src/collective/feedback/tests/test_feedbacks_get.py delete mode 100644 src/collective/feedback/tests/test_robot.py delete mode 100644 src/collective/feedback/tests/test_setup.py create mode 100644 src/collective/feedback/upgrades.py create mode 100644 src/collective/feedback/upgrades.zcml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0bb2ac1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -dist: bionic -language: python -cache: - pip: true - directories: - - eggs - - $HOME/buildout-cache - - $HOME/.buildout -python: - - "2.7" -matrix: - include: - - python: "2.7" - env: PLONE_VERSION=43 - - python: "2.7" - env: PLONE_VERSION=51 - - python: "2.7" - env: PLONE_VERSION=52 - - python: "3.7" - env: PLONE_VERSION=52 - fast_finish: true - -before_install: - - mkdir -p $HOME/buildout-cache/{downloads,eggs,extends} - - mkdir -p $HOME/.buildout - - echo "[buildout]" > $HOME/.buildout/default.cfg - - echo "download-cache = $HOME/buildout-cache/downloads" >> $HOME/.buildout/default.cfg - - echo "eggs-directory = $HOME/buildout-cache/eggs" >> $HOME/.buildout/default.cfg - - echo "extends-cache = $HOME/buildout-cache/extends" >> $HOME/.buildout/default.cfg - - echo "abi-tag-eggs = true" >> $HOME/.buildout/default.cfg - - git config --global user.email "travis@travis-ci.org" - - git config --global user.name "Travis CI" - - sudo apt-get install -y firefox-geckodriver - - virtualenv -p `which python` . - - bin/pip install -r requirements.txt -c constraints_plone$PLONE_VERSION.txt - - cp test_plone$PLONE_VERSION.cfg buildout.cfg - -install: - - travis_retry pip install -U tox coveralls coverage -c constraints.txt - -before_script: - - 'export DISPLAY=:99.0' - - export VERBOSE=true - - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - - sleep 3 - -script: - - PYTEST_ADDOPTS="-s -vv" tox - -after_success: - - python -m coverage.pickle2json - - coverage combine - - coveralls - -notifications: - email: - recipients: -# - travis-reports@plone.com - - {author} - on_success: change - on_failure: change diff --git a/CHANGES.rst b/CHANGES.rst index 4cb32bb..4e185a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,8 +5,15 @@ Changelog 1.0.1 (unreleased) ------------------ -- Nothing changed yet. - +- Only managers can access deleted feedbacks. + [cekk] +- Allow all authenticated users to access @feedback endpoint. + The endpoint will return only feedbacks on objects that they can edit. + [cekk] +- Improve tests. + [cekk] +- Install souper.plone to have its control-panel in backend. + [cekk] 1.0.0 (2023-02-16) ------------------ diff --git a/setup.py b/setup.py index c9c2845..55424f6 100644 --- a/setup.py +++ b/setup.py @@ -53,21 +53,23 @@ python_requires=">=3.7", install_requires=[ "setuptools", - # -*- Extra requirements: -*- - "z3c.jbot", - "plone.api>=1.8.4", - "plone.app.dexterity", "souper.plone", + "collective.honeypot>=2.1", ], extras_require={ "test": [ + "gocept.pytestlayer", "plone.app.testing", - # Plone KGS does not use this version, because it would break - # Remove if your package shall be part of coredev. - # plone_coredev tests as of 2016-04-01. - "plone.testing>=5.0.0", - "plone.app.contenttypes", - "plone.app.robotframework[debug]", + "plone.restapi[test]", + "pytest-cov", + "pytest-plone>=0.2.0", + "pytest-docker", + "pytest-mock", + "pytest", + "zest.releaser[recommended]", + "zestreleaser.towncrier", + "pytest-mock", + "requests-mock", ], }, entry_points=""" diff --git a/src/collective/feedback/configure.zcml b/src/collective/feedback/configure.zcml index bcc1465..fa1ec55 100644 --- a/src/collective/feedback/configure.zcml +++ b/src/collective/feedback/configure.zcml @@ -12,6 +12,8 @@ + + + - + diff --git a/src/collective/feedback/profiles/default/metadata.xml b/src/collective/feedback/profiles/default/metadata.xml index d5bd84a..7640786 100644 --- a/src/collective/feedback/profiles/default/metadata.xml +++ b/src/collective/feedback/profiles/default/metadata.xml @@ -2,6 +2,7 @@ 1000 - + profile-plone.restapi:default + profile-souper.plone:default diff --git a/src/collective/feedback/profiles/default/rolemap.xml b/src/collective/feedback/profiles/default/rolemap.xml index 2739bf4..701d996 100644 --- a/src/collective/feedback/profiles/default/rolemap.xml +++ b/src/collective/feedback/profiles/default/rolemap.xml @@ -12,9 +12,13 @@ + + + + diff --git a/src/collective/feedback/restapi/services/configure.zcml b/src/collective/feedback/restapi/services/configure.zcml index 487ce24..6e84c4e 100644 --- a/src/collective/feedback/restapi/services/configure.zcml +++ b/src/collective/feedback/restapi/services/configure.zcml @@ -17,7 +17,7 @@ method="GET" factory=".get.FeedbackGet" for="plone.app.layout.navigation.interfaces.INavigationRoot" - permission="collective.feedback.AccessFeedbacks" + permission="collective.feedback.FeedbacksOverview" layer="collective.feedback.interfaces.ICollectiveFeedbackLayer" name="@feedback" /> @@ -25,7 +25,7 @@ method="GET" factory=".get.FeedbackGetCSV" for="plone.app.layout.navigation.interfaces.INavigationRoot" - permission="collective.feedback.AccessFeedbacks" + permission="collective.feedback.FeedbacksOverview" layer="collective.feedback.interfaces.ICollectiveFeedbackLayer" name="@feedback-csv" /> diff --git a/src/collective/feedback/restapi/services/get.py b/src/collective/feedback/restapi/services/get.py index ad1cc70..189f0c5 100644 --- a/src/collective/feedback/restapi/services/get.py +++ b/src/collective/feedback/restapi/services/get.py @@ -1,4 +1,5 @@ from AccessControl import Unauthorized +from AccessControl import Unauthorized from collective.feedback.interfaces import ICollectiveFeedbackStore from copy import deepcopy from datetime import datetime @@ -23,39 +24,35 @@ def __init__(self, context, request): super().__init__(context, request) self.params = [] - def publishTraverse(self, request, name): + def publishTraverse(self, request, uid): # Consume any path segments after /@users as parameters - self.params.append(name) + self.params.append(uid) return self def reply(self): if self.params: + # single object detail results = self.get_single_object_feedbacks(self.params[0]) - batch = HypermediaBatch(self.request, results) - data = { - "@id": batch.canonical_url, - "items": [self.fix_fields(x, "date") for x in batch], - "items_total": batch.items_total, - } - links = batch.links - if links: - data["batching"] = links else: results = self.get_data() - batch = HypermediaBatch(self.request, results) - data = { - "@id": batch.canonical_url, - "items": [self.fix_fields(x, "last_vote") for x in batch], - "items_total": batch.items_total, - } - links = batch.links - if links: - data["batching"] = links + batch = HypermediaBatch(self.request, results) + data = { + "@id": batch.canonical_url, + "items": [self.fix_fields(data=x) for x in batch], + "items_total": batch.items_total, + } + links = batch.links + if links: + data["batching"] = links return data - def fix_fields(self, data, param): - data[param] = json_compatible(data[param]) + def fix_fields(self, data): + """ + Make data json compatible + """ + for k, v in data.items(): + data[k] = json_compatible(v) return data def parse_query(self): @@ -75,8 +72,10 @@ def parse_query(self): res["query"] = query return res - def get_commented_obj(self, record): - uid = record._attrs.get("uid", "") + def get_commented_obj(self, uid): + """ + Return obj based on uid. + """ try: obj = api.content.get(UID=uid) except Unauthorized: @@ -86,7 +85,7 @@ def get_commented_obj(self, record): return if not api.user.has_permission( - "rer.customersatisfaction: Access Customer Satisfaction", obj=obj + "collective.feedback: Access Feedbacks", obj=obj ): # user does not have that permission on object return @@ -94,6 +93,12 @@ def get_commented_obj(self, record): return obj def get_single_object_feedbacks(self, uid): + """ + Return data for single object + """ + commented_object = self.get_commented_obj(uid=uid) + if not commented_object: + return [] tool = getUtility(ICollectiveFeedbackStore) results = tool.search(query={"uid": uid}) feedbacks = [] @@ -101,12 +106,12 @@ def get_single_object_feedbacks(self, uid): for record in results: feedbacks.append( { - "uid": record._attrs.get("uid", ""), + "uid": uid, "date": record._attrs.get("date", ""), "vote": record._attrs.get("vote", ""), "answer": record._attrs.get("answer", ""), "comment": record._attrs.get("comment", ""), - "title": record._attrs.get("title", ""), + "title": commented_object.title, } ) @@ -127,12 +132,13 @@ def get_data(self): uid = feedback._attrs.get("uid", "") date = feedback._attrs.get("date", "") vote = feedback._attrs.get("vote", "") - if uid not in feedbacks: - obj = self.get_commented_obj(record=feedback) - if not obj: + obj = self.get_commented_obj(uid=uid) + if not obj and not api.user.has_permission( + "collective.feedback: Show Deleted Feedbacks" + ): + # only manager can list deleted object's reviews continue - new_data = { "vote_num": 0, "vote_sum": 0, @@ -212,7 +218,8 @@ def get_data(self): columns = ["title", "url", "vote", "comment", "date", "answer"] for item in tool.search(): - obj = self.get_commented_obj(record=item) + uid = item._attrs.get("uid", "") + obj = self.get_commented_obj(uid=uid) if not obj: continue diff --git a/src/collective/feedback/testing.py b/src/collective/feedback/testing.py index 3b860ea..3ceb045 100644 --- a/src/collective/feedback/testing.py +++ b/src/collective/feedback/testing.py @@ -1,55 +1,51 @@ -# -*- coding: utf-8 -*- -from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE from plone.app.testing import applyProfile from plone.app.testing import FunctionalTesting from plone.app.testing import IntegrationTesting -from plone.app.testing import PLONE_FIXTURE from plone.app.testing import PloneSandboxLayer -from plone.testing import z2 +from plone.testing.zope import WSGI_SERVER_FIXTURE +from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE import collective.feedback -import plone.app.dexterity import plone.restapi import souper.plone +import collective.honeypot +import collective.honeypot.config +collective.honeypot.config.EXTRA_PROTECTED_ACTIONS = set(["feedback-add"]) +collective.honeypot.config.HONEYPOT_FIELD = "honey" -class CollectiveFeedbackLayer(PloneSandboxLayer): - defaultBases = (PLONE_FIXTURE,) + +class TestLayer(PloneSandboxLayer): + defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,) def setUpZope(self, app, configurationContext): # Load any other ZCML that is required for your tests. # The z3c.autoinclude feature is disabled in the Plone fixture base # layer. - - self.loadZCML(package=plone.app.dexterity) self.loadZCML(package=plone.restapi) - self.loadZCML(package=collective.feedback) + self.loadZCML(package=collective.honeypot) self.loadZCML(package=souper.plone) + self.loadZCML(package=collective.feedback) def setUpPloneSite(self, portal): applyProfile(portal, "collective.feedback:default") -COLLECTIVE_FEEDBACK_FIXTURE = CollectiveFeedbackLayer() +FIXTURE = TestLayer() -COLLECTIVE_FEEDBACK_INTEGRATION_TESTING = IntegrationTesting( - bases=(COLLECTIVE_FEEDBACK_FIXTURE,), +INTEGRATION_TESTING = IntegrationTesting( + bases=(FIXTURE,), name="CollectiveFeedbackLayer:IntegrationTesting", ) -COLLECTIVE_FEEDBACK_FUNCTIONAL_TESTING = FunctionalTesting( - bases=(COLLECTIVE_FEEDBACK_FIXTURE,), +FUNCTIONAL_TESTING = FunctionalTesting( + bases=(FIXTURE,), name="CollectiveFeedbackLayer:FunctionalTesting", ) - -COLLECTIVE_FEEDBACK_ACCEPTANCE_TESTING = FunctionalTesting( - bases=( - COLLECTIVE_FEEDBACK_FIXTURE, - REMOTE_LIBRARY_BUNDLE_FIXTURE, - z2.ZSERVER_FIXTURE, - ), - name="CollectiveFeedbackLayer:AcceptanceTesting", +RESTAPI_TESTING = FunctionalTesting( + bases=(FIXTURE, WSGI_SERVER_FIXTURE), + name="CollectiveFeedbackLayer:RestAPITesting", ) diff --git a/src/collective/feedback/tests/robot/test_example.robot b/src/collective/feedback/tests/robot/test_example.robot deleted file mode 100644 index 93a6c71..0000000 --- a/src/collective/feedback/tests/robot/test_example.robot +++ /dev/null @@ -1,66 +0,0 @@ -# ============================================================================ -# EXAMPLE ROBOT TESTS -# ============================================================================ -# -# Run this robot test stand-alone: -# -# $ bin/test -s collective.feedback -t test_example.robot --all -# -# Run this robot test with robot server (which is faster): -# -# 1) Start robot server: -# -# $ bin/robot-server --reload-path src collective.feedback.testing.COLLECTIVE_FEEDBACK_ACCEPTANCE_TESTING -# -# 2) Run robot tests: -# -# $ bin/robot src/collective/feedback/tests/robot/test_example.robot -# -# See the http://docs.plone.org for further details (search for robot -# framework). -# -# ============================================================================ - -*** Settings ***************************************************************** - -Resource plone/app/robotframework/selenium.robot -Resource plone/app/robotframework/keywords.robot - -Library Remote ${PLONE_URL}/RobotRemote - -Test Setup Open test browser -Test Teardown Close all browsers - - -*** Test Cases *************************************************************** - -Scenario: As a member I want to be able to log into the website - [Documentation] Example of a BDD-style (Behavior-driven development) test. - Given a login form - When I enter valid credentials - Then I am logged in - - -*** Keywords ***************************************************************** - -# --- Given ------------------------------------------------------------------ - -a login form - Go To ${PLONE_URL}/login_form - Wait until page contains Login Name - Wait until page contains Password - - -# --- WHEN ------------------------------------------------------------------- - -I enter valid credentials - Input Text __ac_name admin - Input Text __ac_password secret - Click Button Log in - - -# --- THEN ------------------------------------------------------------------- - -I am logged in - Wait until page contains You are now logged in - Page should contain You are now logged in diff --git a/src/collective/feedback/tests/test_delete_content.py b/src/collective/feedback/tests/test_delete_content.py new file mode 100644 index 0000000..af01010 --- /dev/null +++ b/src/collective/feedback/tests/test_delete_content.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +from collective.feedback.testing import RESTAPI_TESTING +from plone import api +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.restapi.testing import RelativeSession +from souper.soup import get_soup +from souper.soup import Record +from zope.component import getUtility +from collective.feedback.interfaces import ICollectiveFeedbackStore +from datetime import datetime + +import transaction +import unittest + + +class TestCustomerSatisfactionGet(unittest.TestCase): + layer = RESTAPI_TESTING + + def add_record(self, date=None, vote="", uid="", comment="", title=""): + if not date: + date = datetime.now() + soup = get_soup("feedback_soup", self.portal) + transaction.commit() + record = Record() + record.attrs["vote"] = vote + record.attrs["date"] = date + + if comment: + record.attrs["comment"] = comment + if uid: + record.attrs["uid"] = uid + if title: + record.attrs["title"] = title + soup.add(record) + transaction.commit() + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.portal_url = self.portal.absolute_url() + self.url = "{}/@feedback".format(self.portal_url) + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + self.document1 = api.content.create( + title="document 1", container=self.portal, type="Document" + ) + self.document2 = api.content.create( + title="document 1", container=self.portal, type="Document" + ) + transaction.commit() + + # add some reviews + tool = getUtility(ICollectiveFeedbackStore) + tool.add( + { + "vote": 1, + "uid": self.document1.UID(), + "title": self.document1.title, + } + ) + tool.add( + { + "vote": 2, + "uid": self.document1.UID(), + "title": self.document1.title, + } + ) + tool.add( + { + "vote": 3, + "uid": self.document2.UID(), + "title": self.document2.title, + } + ) + + self.api_session = RelativeSession(self.portal_url) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + transaction.commit() + + def tearDown(self): + self.api_session.close() + soup = get_soup("feedback_soup", self.portal) + soup.clear() + + def test_deleting_a_content_does_not_remove_entries(self): + response = self.api_session.get(self.url) + res = response.json() + self.assertEqual(res["items_total"], 2) + + api.content.delete(obj=self.document1) + transaction.commit() + + response = self.api_session.get(self.url) + res = response.json() + self.assertEqual(res["items_total"], 2) diff --git a/src/collective/feedback/tests/test_feedbacks_add.py b/src/collective/feedback/tests/test_feedbacks_add.py new file mode 100644 index 0000000..65b3ea5 --- /dev/null +++ b/src/collective/feedback/tests/test_feedbacks_add.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +from collective.feedback.testing import RESTAPI_TESTING +from plone import api +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from collective.feedback.interfaces import ICollectiveFeedbackStore +from plone.restapi.testing import RelativeSession +from zope.component import getUtility + +import transaction +import unittest + + +class TestAdd(unittest.TestCase): + layer = RESTAPI_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + api.user.create( + email="memberuser@example.com", + username="memberuser", + password="secret!!", + ) + + self.document = api.content.create( + title="Document", container=self.portal, type="Document" + ) + api.content.transition(obj=self.document, transition="publish") + + self.private_document = api.content.create( + title="restricted document", container=self.portal, type="Document" + ) + transaction.commit() + + self.api_session = RelativeSession(self.portal_url) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + self.anon_api_session = RelativeSession(self.portal_url) + self.anon_api_session.headers.update({"Accept": "application/json"}) + + self.url = "{}/@feedback-add".format(self.document.absolute_url()) + self.url_private_document = "{}/@feedback-add".format( + self.private_document.absolute_url() + ) + + def tearDown(self): + self.api_session.close() + self.anon_api_session.close() + + def test_required_params(self): + """ """ + # vote is required + res = self.anon_api_session.post(self.url, json={"honey": ""}) + self.assertEqual(res.status_code, 400) + self.assertEqual(res.json()["message"], "Campo obbligatorio mancante: vote") + + def test_correctly_save_data(self): + self.anon_api_session.post( + self.url, + json={"vote": 3, "comment": "i disagree", "honey": ""}, + ) + transaction.commit() + tool = getUtility(ICollectiveFeedbackStore) + self.assertEqual(len(tool.search()), 1) + + # Anonymous cannot vote without access to document + self.anon_api_session.post( + self.url_private_document, + json={"vote": 2, "comment": "i disagree", "honey": ""}, + ) + transaction.commit() + # Number of results did not increase, cause user is unauthorized to vote + self.assertEqual(len(tool.search()), 1) + + def test_store_only_known_fields(self): + self.anon_api_session.post( + self.url, + json={ + "vote": "nok", + "comment": "i disagree", + "unknown": "mistery", + "honey": "", + }, + ) + transaction.commit() + tool = getUtility(ICollectiveFeedbackStore) + res = tool.search() + self.assertEqual(len(res), 1) + self.assertEqual(res[0]._attrs.get("unknown", None), None) + self.assertEqual(res[0]._attrs.get("vote", None), "nok") + self.assertEqual(res[0]._attrs.get("comment", None), "i disagree") + + def test_honeypot_is_required(self): + res = self.anon_api_session.post(self.url, json={}) + self.assertEqual(res.status_code, 403) + + res = self.anon_api_session.post(self.url, json={"vote": "ok"}) + self.assertEqual(res.status_code, 403) + + # HONEYPOT_FIELD is set in testing.py + + res = self.anon_api_session.post(self.url, json={"vote": "ok", "honey": ""}) + self.assertEqual(res.status_code, 204) + + # this is compiled by a bot + res = self.anon_api_session.post( + self.url, json={"vote": "ok", "honey": "i'm a bot"} + ) + self.assertEqual(res.status_code, 403) diff --git a/src/collective/feedback/tests/test_feedbacks_get.py b/src/collective/feedback/tests/test_feedbacks_get.py new file mode 100644 index 0000000..7c8cc14 --- /dev/null +++ b/src/collective/feedback/tests/test_feedbacks_get.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +from collective.feedback.testing import RESTAPI_TESTING +from datetime import datetime +from plone import api +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.restapi.serializer.converters import json_compatible +from plone.restapi.testing import RelativeSession +from souper.soup import get_soup +from souper.soup import Record +from collective.feedback.interfaces import ICollectiveFeedbackStore +from zope.component import getUtility + +import transaction +import unittest + + +class TestGet(unittest.TestCase): + layer = RESTAPI_TESTING + + def add_record(self, date=None, vote="", uid="", comment="", title=""): + if not date: + date = datetime.now() + soup = get_soup("feedback_soup", self.portal) + transaction.commit() + record = Record() + record.attrs["vote"] = vote + record.attrs["date"] = date + + if comment: + record.attrs["comment"] = comment + if uid: + record.attrs["uid"] = uid + if title: + record.attrs["title"] = title + soup.add(record) + transaction.commit() + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.portal_url = self.portal.absolute_url() + self.url = "{}/@feedback".format(self.portal_url) + self.tool = getUtility(ICollectiveFeedbackStore) + + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + api.user.create( + email="member@example.com", + username="member", + password="secret!!", + ) + api.user.create( + email="global@example.com", + username="global", + password="secret!!", + ) + + api.user.create( + email="local@example.com", + username="local", + password="secret!!", + ) + # create some contents + self.document = api.content.create( + title="document", container=self.portal, type="Document" + ) + api.content.transition(obj=self.document, transition="publish") + + self.restricted_document = api.content.create( + title="restricted document", container=self.portal, type="Document" + ) + + transaction.commit() + + api.user.grant_roles( + username="global", + roles=["Editor"], + ) + api.user.grant_roles( + username="local", roles=["Editor"], obj=self.restricted_document + ) + + self.api_session = RelativeSession(self.portal_url) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + transaction.commit() + + def tearDown(self): + self.api_session.close() + self.tool.clear() + + def test_anon_cant_access_endpoint(self): + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + self.assertEqual(api_session.get(self.url).status_code, 401) + + def test_admin_can_access_endpoint(self): + self.assertEqual(self.api_session.get(self.url).status_code, 200) + + def test_other_users_can_access_endpoint(self): + for username in ["member", "local", "global"]: + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + api_session.auth = (username, "secret!!") + self.assertEqual(api_session.get(self.url).status_code, 200) + + def test_endpoint_returns_data(self): + response = self.api_session.get(self.url) + res = response.json() + self.assertEqual(res["items_total"], 0) + now = datetime.now() + self.add_record(vote=1, comment="is ok", date=now) + + response = self.api_session.get(self.url) + res = response.json() + + self.assertEqual(res["items_total"], 1) + self.assertEqual( + res["items"], + [ + { + "comments": 1, + "last_vote": json_compatible(now), + "title": "", + "uid": "", + "vote": 1.0, + } + ], + ) + + def test_basic_user_cant_see_data(self): + now = datetime.now() + self.add_record( + vote=1, date=now, comment="is ok for member", uid=self.document.UID() + ) + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + api_session.auth = ("member", "secret!!") + + response = api_session.get(self.url) + res = response.json() + + self.assertEqual(res["items_total"], 0) + + def test_global_editor_can_see_all_data(self): + now = datetime.now() + self.add_record( + vote=1, date=now, comment="is ok for global", uid=self.document.UID() + ) + self.add_record( + vote=1, + date=now, + comment="ok also for restricted", + uid=self.restricted_document.UID(), + ) + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + api_session.auth = ("global", "secret!!") + + response = api_session.get(self.url) + res = response.json() + + self.assertEqual(res["items_total"], 2) + + def test_local_editor_can_see_only_data_for_his_contents(self): + now = datetime.now() + self.add_record( + vote=1, date=now, comment="is ok for global", uid=self.document.UID() + ) + self.add_record( + vote=1, + date=now, + comment="ok also for restricted", + uid=self.restricted_document.UID(), + ) + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + api_session.auth = ("local", "secret!!") + + response = api_session.get(self.url) + res = response.json() + + self.assertEqual(res["items_total"], 1) + self.assertEqual(res["items"][0]["uid"], self.restricted_document.UID()) + + def test_only_admins_can_see_deleted_contents(self): + now = datetime.now() + self.add_record(vote=1, date=now, comment="is ok", uid=self.document.UID()) + self.add_record( + vote=1, + date=now, + comment="ok for deleted content", + uid="qwertyuiop", + ) + + response = self.api_session.get(self.url) + res = response.json() + self.assertEqual(res["items_total"], 2) + + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + api_session.auth = ("global", "secret!!") + + response = api_session.get(self.url) + res = response.json() + + self.assertEqual(res["items_total"], 1) diff --git a/src/collective/feedback/tests/test_robot.py b/src/collective/feedback/tests/test_robot.py deleted file mode 100644 index 6e1f6bb..0000000 --- a/src/collective/feedback/tests/test_robot.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -from collective.feedback.testing import ( # noqa: E501 - COLLECTIVE_FEEDBACK_ACCEPTANCE_TESTING, -) -from plone.app.testing import ROBOT_TEST_LEVEL -from plone.testing import layered - -import os -import robotsuite -import unittest - - -def test_suite(): - suite = unittest.TestSuite() - current_dir = os.path.abspath(os.path.dirname(__file__)) - robot_dir = os.path.join(current_dir, "robot") - robot_tests = [ - os.path.join("robot", doc) - for doc in os.listdir(robot_dir) - if doc.endswith(".robot") and doc.startswith("test_") - ] - for robot_test in robot_tests: - robottestsuite = robotsuite.RobotTestSuite(robot_test) - robottestsuite.level = ROBOT_TEST_LEVEL - suite.addTests( - [ - layered( - robottestsuite, - layer=COLLECTIVE_FEEDBACK_ACCEPTANCE_TESTING, - ), - ] - ) - return suite diff --git a/src/collective/feedback/tests/test_setup.py b/src/collective/feedback/tests/test_setup.py deleted file mode 100644 index 1bcdc71..0000000 --- a/src/collective/feedback/tests/test_setup.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -"""Setup tests for this package.""" -from collective.feedback.testing import ( # noqa: E501 - COLLECTIVE_FEEDBACK_INTEGRATION_TESTING, -) -from plone import api -from plone.app.testing import setRoles -from plone.app.testing import TEST_USER_ID - -import unittest - - -try: - from Products.CMFPlone.utils import get_installer -except ImportError: - get_installer = None - - -class TestSetup(unittest.TestCase): - """Test that collective.feedback is properly installed.""" - - layer = COLLECTIVE_FEEDBACK_INTEGRATION_TESTING - - def setUp(self): - """Custom shared utility setup for tests.""" - self.portal = self.layer["portal"] - if get_installer: - self.installer = get_installer(self.portal, self.layer["request"]) - else: - self.installer = api.portal.get_tool("portal_quickinstaller") - - def test_product_installed(self): - """Test if collective.feedback is installed.""" - self.assertTrue(self.installer.is_product_installed("collective.feedback")) - - def test_browserlayer(self): - """Test that ICollectiveFeedbackLayer is registered.""" - from collective.feedback.interfaces import ICollectiveFeedbackLayer - from plone.browserlayer import utils - - self.assertIn(ICollectiveFeedbackLayer, utils.registered_layers()) - - -class TestUninstall(unittest.TestCase): - layer = COLLECTIVE_FEEDBACK_INTEGRATION_TESTING - - def setUp(self): - self.portal = self.layer["portal"] - if get_installer: - self.installer = get_installer(self.portal, self.layer["request"]) - else: - self.installer = api.portal.get_tool("portal_quickinstaller") - roles_before = api.user.get_roles(TEST_USER_ID) - setRoles(self.portal, TEST_USER_ID, ["Manager"]) - self.installer.uninstall_product("collective.feedback") - setRoles(self.portal, TEST_USER_ID, roles_before) - - def test_product_uninstalled(self): - """Test if collective.feedback is cleanly uninstalled.""" - self.assertFalse(self.installer.is_product_installed("collective.feedback")) - - def test_browserlayer_removed(self): - """Test that ICollectiveFeedbackLayer is removed.""" - from collective.feedback.interfaces import ICollectiveFeedbackLayer - from plone.browserlayer import utils - - self.assertNotIn(ICollectiveFeedbackLayer, utils.registered_layers()) diff --git a/src/collective/feedback/tests/test_store.py b/src/collective/feedback/tests/test_store.py index 815d2a7..1ffaffa 100644 --- a/src/collective/feedback/tests/test_store.py +++ b/src/collective/feedback/tests/test_store.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from collective.feedback.interfaces import ICollectiveFeedbackStore -from collective.feedback.testing import COLLECTIVE_FEEDBACK_FUNCTIONAL_TESTING +from collective.feedback.testing import FUNCTIONAL_TESTING from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID from zope.component import getUtility @@ -10,7 +10,7 @@ class TestTool(unittest.TestCase): - layer = COLLECTIVE_FEEDBACK_FUNCTIONAL_TESTING + layer = FUNCTIONAL_TESTING def setUp(self): self.app = self.layer["app"] diff --git a/src/collective/feedback/upgrades.py b/src/collective/feedback/upgrades.py new file mode 100644 index 0000000..3204dcc --- /dev/null +++ b/src/collective/feedback/upgrades.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from Acquisition import aq_base +from collective.volto.blocksfield.field import BlocksField +from copy import deepcopy +from design.plone.policy.setuphandlers import disable_searchable_types +from design.plone.policy.setuphandlers import set_default_subsite_colors +from design.plone.policy.utils import create_default_blocks +from design.plone.policy.interfaces import IDesignPlonePolicySettings +from plone import api +from plone.app.upgrade.utils import installOrReinstallProduct +from plone.dexterity.utils import iterSchemata +from plone.registry.interfaces import IRegistry +from plone.restapi.behaviors import IBlocks +from Products.CMFPlone.interfaces import IFilterSchema +from Products.CMFPlone.interfaces import ISelectableConstrainTypes +from zope.component import getUtility +from zope.schema import getFields + + +import logging + + +logger = logging.getLogger(__name__) + +DEFAULT_PROFILE = "profile-collective.feedback:default" + + +def update_profile(context, profile, run_dependencies=True): + context.runImportStepFromProfile(DEFAULT_PROFILE, profile, run_dependencies) + + +def update_types(context): + update_profile(context, "typeinfo") + + +def update_rolemap(context): + update_profile(context, "rolemap") + + +def update_registry(context): + update_profile(context, "plone.app.registry", run_dependencies=False) + + +def update_catalog(context): + update_profile(context, "catalog") + + +def update_controlpanel(context): + update_profile(context, "controlpanel") + + +def to_1100(context): + installOrReinstallProduct(api.portal.get(), "souper.plone") diff --git a/src/collective/feedback/upgrades.zcml b/src/collective/feedback/upgrades.zcml new file mode 100644 index 0000000..cae2d8a --- /dev/null +++ b/src/collective/feedback/upgrades.zcml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/test_plone60.cfg b/test_plone60.cfg index 7dc9271..dd7eb43 100644 --- a/test_plone60.cfg +++ b/test_plone60.cfg @@ -54,3 +54,38 @@ plumber = 1.7 # Required by: # souper==1.1.1 node.ext.zodb = 1.5 + +# Added by buildout at 2024-02-08 15:13:37.168123 +build = 1.0.3 +check-manifest = 0.49 +pep440 = 0.1.2 +pyproject-hooks = 1.0.0 +pyroma = 4.2 +pytest = 7.4.3 +pytest-cov = 4.1.0 +pytest-docker = 2.0.1 +pytest-mock = 3.12.0 +pytest-plone = 0.2.0 +requests-mock = 1.11.0 +towncrier = 23.6.0 +trove-classifiers = 2023.8.7 +zestreleaser.towncrier = 1.3.0 + +# Required by: +# towncrier==23.6.0 +click-default-group = 1.2.4 + +# Required by: +# pytest-plone==0.2.0 +gocept.pytestlayer = 8.1 + +# Required by: +# towncrier==23.6.0 +incremental = 22.10.0 + +# Required by: +# pytest==7.4.3 +iniconfig = 2.0.0 + +# Added by buildout at 2024-02-08 16:09:12.800009 +collective.honeypot = 2.1 From e9a43cbeef3bf48779bdc0dba6dd2ac7364dafea Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Thu, 8 Feb 2024 21:50:14 +0100 Subject: [PATCH 2/6] zpretty --- base.cfg | 16 ++++++++++++++++ src/collective/feedback/permissions.zcml | 4 ++-- src/collective/feedback/upgrades.zcml | 2 +- test_plone60.cfg | 3 +++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/base.cfg b/base.cfg index efbbe11..80a3979 100644 --- a/base.cfg +++ b/base.cfg @@ -17,6 +17,8 @@ parts = robot plone-helper-scripts vscode + zpretty + zpretty-run develop = . @@ -109,6 +111,20 @@ scripts = zopepy plone-compile-resources +[zpretty] +recipe = zc.recipe.egg +eggs = + zpretty + +[zpretty-run] +recipe = collective.recipe.template +input = inline: + #!/bin/bash + find src -name '*.zcml' | xargs bin/zpretty -i +output = ${buildout:directory}/bin/zpretty-run +mode = 755 + + [versions] # Don't use a released version of collective.feedback collective.feedback = diff --git a/src/collective/feedback/permissions.zcml b/src/collective/feedback/permissions.zcml index cda1597..74ee03a 100644 --- a/src/collective/feedback/permissions.zcml +++ b/src/collective/feedback/permissions.zcml @@ -13,10 +13,10 @@ id="collective.feedback.AccessFeedbacks" title="collective.feedback: Access Feedbacks" /> - - + diff --git a/src/collective/feedback/upgrades.zcml b/src/collective/feedback/upgrades.zcml index cae2d8a..cc7e2c1 100644 --- a/src/collective/feedback/upgrades.zcml +++ b/src/collective/feedback/upgrades.zcml @@ -12,7 +12,7 @@ title="Update rolemap" handler=".upgrades.update_rolemap" /> - diff --git a/test_plone60.cfg b/test_plone60.cfg index dd7eb43..12e1d6f 100644 --- a/test_plone60.cfg +++ b/test_plone60.cfg @@ -89,3 +89,6 @@ iniconfig = 2.0.0 # Added by buildout at 2024-02-08 16:09:12.800009 collective.honeypot = 2.1 + +# Added by buildout at 2024-02-08 21:49:12.973900 +zpretty = 3.1.0 From 5cb1041912ea146b72736dd21394809e61aa3e2e Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Thu, 8 Feb 2024 21:51:39 +0100 Subject: [PATCH 3/6] isort --- src/collective/feedback/restapi/services/get.py | 1 - src/collective/feedback/testing.py | 7 ++++--- src/collective/feedback/tests/test_delete_content.py | 4 ++-- src/collective/feedback/tests/test_feedbacks_add.py | 2 +- src/collective/feedback/tests/test_feedbacks_get.py | 2 +- src/collective/feedback/upgrades.py | 3 +-- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/collective/feedback/restapi/services/get.py b/src/collective/feedback/restapi/services/get.py index 189f0c5..dc7a7fa 100644 --- a/src/collective/feedback/restapi/services/get.py +++ b/src/collective/feedback/restapi/services/get.py @@ -1,5 +1,4 @@ from AccessControl import Unauthorized -from AccessControl import Unauthorized from collective.feedback.interfaces import ICollectiveFeedbackStore from copy import deepcopy from datetime import datetime diff --git a/src/collective/feedback/testing.py b/src/collective/feedback/testing.py index 3ceb045..a04d653 100644 --- a/src/collective/feedback/testing.py +++ b/src/collective/feedback/testing.py @@ -1,15 +1,16 @@ +from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE from plone.app.testing import applyProfile from plone.app.testing import FunctionalTesting from plone.app.testing import IntegrationTesting from plone.app.testing import PloneSandboxLayer from plone.testing.zope import WSGI_SERVER_FIXTURE -from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE import collective.feedback -import plone.restapi -import souper.plone import collective.honeypot import collective.honeypot.config +import plone.restapi +import souper.plone + collective.honeypot.config.EXTRA_PROTECTED_ACTIONS = set(["feedback-add"]) collective.honeypot.config.HONEYPOT_FIELD = "honey" diff --git a/src/collective/feedback/tests/test_delete_content.py b/src/collective/feedback/tests/test_delete_content.py index af01010..a17a00c 100644 --- a/src/collective/feedback/tests/test_delete_content.py +++ b/src/collective/feedback/tests/test_delete_content.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from collective.feedback.interfaces import ICollectiveFeedbackStore from collective.feedback.testing import RESTAPI_TESTING +from datetime import datetime from plone import api from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME @@ -9,8 +11,6 @@ from souper.soup import get_soup from souper.soup import Record from zope.component import getUtility -from collective.feedback.interfaces import ICollectiveFeedbackStore -from datetime import datetime import transaction import unittest diff --git a/src/collective/feedback/tests/test_feedbacks_add.py b/src/collective/feedback/tests/test_feedbacks_add.py index 65b3ea5..5d20231 100644 --- a/src/collective/feedback/tests/test_feedbacks_add.py +++ b/src/collective/feedback/tests/test_feedbacks_add.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- +from collective.feedback.interfaces import ICollectiveFeedbackStore from collective.feedback.testing import RESTAPI_TESTING from plone import api from plone.app.testing import setRoles from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID -from collective.feedback.interfaces import ICollectiveFeedbackStore from plone.restapi.testing import RelativeSession from zope.component import getUtility diff --git a/src/collective/feedback/tests/test_feedbacks_get.py b/src/collective/feedback/tests/test_feedbacks_get.py index 7c8cc14..5f565e6 100644 --- a/src/collective/feedback/tests/test_feedbacks_get.py +++ b/src/collective/feedback/tests/test_feedbacks_get.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from collective.feedback.interfaces import ICollectiveFeedbackStore from collective.feedback.testing import RESTAPI_TESTING from datetime import datetime from plone import api @@ -10,7 +11,6 @@ from plone.restapi.testing import RelativeSession from souper.soup import get_soup from souper.soup import Record -from collective.feedback.interfaces import ICollectiveFeedbackStore from zope.component import getUtility import transaction diff --git a/src/collective/feedback/upgrades.py b/src/collective/feedback/upgrades.py index 3204dcc..e3575c6 100644 --- a/src/collective/feedback/upgrades.py +++ b/src/collective/feedback/upgrades.py @@ -2,10 +2,10 @@ from Acquisition import aq_base from collective.volto.blocksfield.field import BlocksField from copy import deepcopy +from design.plone.policy.interfaces import IDesignPlonePolicySettings from design.plone.policy.setuphandlers import disable_searchable_types from design.plone.policy.setuphandlers import set_default_subsite_colors from design.plone.policy.utils import create_default_blocks -from design.plone.policy.interfaces import IDesignPlonePolicySettings from plone import api from plone.app.upgrade.utils import installOrReinstallProduct from plone.dexterity.utils import iterSchemata @@ -16,7 +16,6 @@ from zope.component import getUtility from zope.schema import getFields - import logging From 853032d9475bfa5fd51dcb63fb396207935cade3 Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Thu, 8 Feb 2024 21:52:28 +0100 Subject: [PATCH 4/6] flake8 --- src/collective/feedback/upgrades.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/collective/feedback/upgrades.py b/src/collective/feedback/upgrades.py index e3575c6..bbdc324 100644 --- a/src/collective/feedback/upgrades.py +++ b/src/collective/feedback/upgrades.py @@ -1,20 +1,6 @@ # -*- coding: utf-8 -*- -from Acquisition import aq_base -from collective.volto.blocksfield.field import BlocksField -from copy import deepcopy -from design.plone.policy.interfaces import IDesignPlonePolicySettings -from design.plone.policy.setuphandlers import disable_searchable_types -from design.plone.policy.setuphandlers import set_default_subsite_colors -from design.plone.policy.utils import create_default_blocks from plone import api from plone.app.upgrade.utils import installOrReinstallProduct -from plone.dexterity.utils import iterSchemata -from plone.registry.interfaces import IRegistry -from plone.restapi.behaviors import IBlocks -from Products.CMFPlone.interfaces import IFilterSchema -from Products.CMFPlone.interfaces import ISelectableConstrainTypes -from zope.component import getUtility -from zope.schema import getFields import logging From 328f8803efb9b3062f6fed60381807caf527a5b6 Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Fri, 9 Feb 2024 17:11:10 +0100 Subject: [PATCH 5/6] fix upgrade-step, remote unused user action and add actions to @feedbacks endpoint --- CHANGES.rst | 4 +++ src/collective/feedback/configure.zcml | 10 ++++++- src/collective/feedback/permissions.zcml | 9 +++++-- .../feedback/profiles/default/actions.xml | 27 ------------------- .../feedback/profiles/default/metadata.xml | 2 +- .../feedback/profiles/default/rolemap.xml | 8 +++++- .../profiles/default/to_1100/rolemap.xml | 6 +++++ .../feedback/restapi/services/configure.zcml | 2 +- .../feedback/restapi/services/get.py | 4 +++ src/collective/feedback/setuphandlers.py | 1 + .../feedback/tests/test_feedbacks_get.py | 24 +++++++++++++++++ src/collective/feedback/upgrades.py | 5 ++++ 12 files changed, 69 insertions(+), 33 deletions(-) delete mode 100644 src/collective/feedback/profiles/default/actions.xml create mode 100644 src/collective/feedback/profiles/default/to_1100/rolemap.xml diff --git a/CHANGES.rst b/CHANGES.rst index 4e185a3..6d65af8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,10 @@ Changelog [cekk] - Install souper.plone to have its control-panel in backend. [cekk] +- Remove unused user action. + [cekk] +- Add `actions` infos in @feedback endpoint, to let the frontend know what the user can do. + [cekk] 1.0.0 (2023-02-16) ------------------ diff --git a/src/collective/feedback/configure.zcml b/src/collective/feedback/configure.zcml index fa1ec55..5e3e5f4 100644 --- a/src/collective/feedback/configure.zcml +++ b/src/collective/feedback/configure.zcml @@ -9,10 +9,10 @@ + - + + + + diff --git a/src/collective/feedback/profiles/default/actions.xml b/src/collective/feedback/profiles/default/actions.xml deleted file mode 100644 index 75411c0..0000000 --- a/src/collective/feedback/profiles/default/actions.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - Feedback dashboard - string:${globals_view/navigationRootUrl}/feedback-dashboard - - - - - - True - - - - diff --git a/src/collective/feedback/profiles/default/metadata.xml b/src/collective/feedback/profiles/default/metadata.xml index 7640786..37cad1a 100644 --- a/src/collective/feedback/profiles/default/metadata.xml +++ b/src/collective/feedback/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 1000 + 1100 profile-plone.restapi:default profile-souper.plone:default diff --git a/src/collective/feedback/profiles/default/rolemap.xml b/src/collective/feedback/profiles/default/rolemap.xml index 701d996..cfdb62b 100644 --- a/src/collective/feedback/profiles/default/rolemap.xml +++ b/src/collective/feedback/profiles/default/rolemap.xml @@ -3,7 +3,7 @@ @@ -20,5 +20,11 @@ + + + + + + diff --git a/src/collective/feedback/profiles/default/to_1100/rolemap.xml b/src/collective/feedback/profiles/default/to_1100/rolemap.xml new file mode 100644 index 0000000..1bad339 --- /dev/null +++ b/src/collective/feedback/profiles/default/to_1100/rolemap.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/collective/feedback/restapi/services/configure.zcml b/src/collective/feedback/restapi/services/configure.zcml index 6e84c4e..c80e9b3 100644 --- a/src/collective/feedback/restapi/services/configure.zcml +++ b/src/collective/feedback/restapi/services/configure.zcml @@ -33,7 +33,7 @@ method="DELETE" factory=".delete.FeedbackDelete" for="plone.app.layout.navigation.interfaces.INavigationRoot" - permission="collective.feedback.ManageFeedbacks" + permission="collective.feedback.DeleteFeedbacks" layer="collective.feedback.interfaces.ICollectiveFeedbackLayer" name="@feedback-delete" /> diff --git a/src/collective/feedback/restapi/services/get.py b/src/collective/feedback/restapi/services/get.py index dc7a7fa..1cdd527 100644 --- a/src/collective/feedback/restapi/services/get.py +++ b/src/collective/feedback/restapi/services/get.py @@ -44,8 +44,12 @@ def reply(self): if links: data["batching"] = links + data["actions"] = {"can_delete_feedbacks": self.can_delete_feedbacks()} return data + def can_delete_feedbacks(self): + return api.user.has_permission("collective.feedback: Delete Feedbacks") + def fix_fields(self, data): """ Make data json compatible diff --git a/src/collective/feedback/setuphandlers.py b/src/collective/feedback/setuphandlers.py index e923759..f77ff3f 100644 --- a/src/collective/feedback/setuphandlers.py +++ b/src/collective/feedback/setuphandlers.py @@ -9,6 +9,7 @@ def getNonInstallableProfiles(self): """Hide uninstall profile from site-creation and quickinstaller.""" return [ "collective.feedback:uninstall", + "collective.feedback:to_1100", ] def getNonInstallableProducts(self): diff --git a/src/collective/feedback/tests/test_feedbacks_get.py b/src/collective/feedback/tests/test_feedbacks_get.py index 5f565e6..c283acb 100644 --- a/src/collective/feedback/tests/test_feedbacks_get.py +++ b/src/collective/feedback/tests/test_feedbacks_get.py @@ -209,3 +209,27 @@ def test_only_admins_can_see_deleted_contents(self): res = response.json() self.assertEqual(res["items_total"], 1) + + def test_actions_list_returned(self): + response = self.api_session.get(self.url) + res = response.json() + self.assertIn("actions", res) + + def test_users_with_permission_have_can_delete_feedbacks_action(self): + + response = self.api_session.get(self.url) + res = response.json() + self.assertIn("can_delete_feedbacks", res["actions"]) + self.assertTrue(res["actions"]["can_delete_feedbacks"]) + + def test_users_without_permission_dont_have_can_delete_feedbacks_action(self): + + api_session = RelativeSession(self.portal_url) + api_session.headers.update({"Accept": "application/json"}) + api_session.auth = ("global", "secret!!") + + response = api_session.get(self.url) + res = response.json() + + self.assertIn("can_delete_feedbacks", res["actions"]) + self.assertFalse(res["actions"]["can_delete_feedbacks"]) diff --git a/src/collective/feedback/upgrades.py b/src/collective/feedback/upgrades.py index bbdc324..757c8b4 100644 --- a/src/collective/feedback/upgrades.py +++ b/src/collective/feedback/upgrades.py @@ -36,3 +36,8 @@ def update_controlpanel(context): def to_1100(context): installOrReinstallProduct(api.portal.get(), "souper.plone") + context.runAllImportStepsFromProfile("profile-collective.feedback:to_1100") + + # remove broken action + if "feedback-dashboard" in context.portal_actions.user: + del context.portal_actions.user["feedback-dashboard"] From 91ae67885b9c4b96e693eb412ffa061549bd62d6 Mon Sep 17 00:00:00 2001 From: Andrea Cecchi Date: Fri, 9 Feb 2024 17:11:35 +0100 Subject: [PATCH 6/6] cleanup code --- src/collective/feedback/permissions.zcml | 2 +- src/collective/feedback/tests/test_feedbacks_get.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/collective/feedback/permissions.zcml b/src/collective/feedback/permissions.zcml index 5c0c6a0..ee69ca2 100644 --- a/src/collective/feedback/permissions.zcml +++ b/src/collective/feedback/permissions.zcml @@ -21,7 +21,7 @@ id="collective.feedback.ShowDeletedFeedbacks" title="collective.feedback: Show Deleted Feedbacks" /> - + diff --git a/src/collective/feedback/tests/test_feedbacks_get.py b/src/collective/feedback/tests/test_feedbacks_get.py index c283acb..6f413e0 100644 --- a/src/collective/feedback/tests/test_feedbacks_get.py +++ b/src/collective/feedback/tests/test_feedbacks_get.py @@ -216,14 +216,12 @@ def test_actions_list_returned(self): self.assertIn("actions", res) def test_users_with_permission_have_can_delete_feedbacks_action(self): - response = self.api_session.get(self.url) res = response.json() self.assertIn("can_delete_feedbacks", res["actions"]) self.assertTrue(res["actions"]["can_delete_feedbacks"]) def test_users_without_permission_dont_have_can_delete_feedbacks_action(self): - api_session = RelativeSession(self.portal_url) api_session.headers.update({"Accept": "application/json"}) api_session.auth = ("global", "secret!!")