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..6d65af8 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,8 +5,19 @@ 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]
+- 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/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/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..5e3e5f4 100644
--- a/src/collective/feedback/configure.zcml
+++ b/src/collective/feedback/configure.zcml
@@ -9,10 +9,12 @@
+
+
+
+
+
+
+
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 @@
-
-
diff --git a/src/collective/feedback/profiles/default/metadata.xml b/src/collective/feedback/profiles/default/metadata.xml
index d5bd84a..37cad1a 100644
--- a/src/collective/feedback/profiles/default/metadata.xml
+++ b/src/collective/feedback/profiles/default/metadata.xml
@@ -1,7 +1,8 @@
- 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 2739bf4..cfdb62b 100644
--- a/src/collective/feedback/profiles/default/rolemap.xml
+++ b/src/collective/feedback/profiles/default/rolemap.xml
@@ -3,7 +3,7 @@
@@ -12,9 +12,19 @@
+
+
+
+
+
+
+
+
+
+
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 487ce24..c80e9b3 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"
/>
@@ -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 ad1cc70..1cdd527 100644
--- a/src/collective/feedback/restapi/services/get.py
+++ b/src/collective/feedback/restapi/services/get.py
@@ -23,39 +23,39 @@ 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
+
+ data["actions"] = {"can_delete_feedbacks": self.can_delete_feedbacks()}
return data
- def fix_fields(self, data, param):
- data[param] = json_compatible(data[param])
+ def can_delete_feedbacks(self):
+ return api.user.has_permission("collective.feedback: Delete Feedbacks")
+
+ 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 +75,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 +88,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 +96,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 +109,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 +135,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 +221,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/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/testing.py b/src/collective/feedback/testing.py
index 3b860ea..a04d653 100644
--- a/src/collective/feedback/testing.py
+++ b/src/collective/feedback/testing.py
@@ -1,55 +1,52 @@
-# -*- coding: utf-8 -*-
-from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE
+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 PLONE_FIXTURE
from plone.app.testing import PloneSandboxLayer
-from plone.testing import z2
+from plone.testing.zope import WSGI_SERVER_FIXTURE
import collective.feedback
-import plone.app.dexterity
+import collective.honeypot
+import collective.honeypot.config
import plone.restapi
import souper.plone
-class CollectiveFeedbackLayer(PloneSandboxLayer):
- defaultBases = (PLONE_FIXTURE,)
+collective.honeypot.config.EXTRA_PROTECTED_ACTIONS = set(["feedback-add"])
+collective.honeypot.config.HONEYPOT_FIELD = "honey"
+
+
+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..a17a00c
--- /dev/null
+++ b/src/collective/feedback/tests/test_delete_content.py
@@ -0,0 +1,100 @@
+# -*- 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
+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
+
+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..5d20231
--- /dev/null
+++ b/src/collective/feedback/tests/test_feedbacks_add.py
@@ -0,0 +1,115 @@
+# -*- 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 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..6f413e0
--- /dev/null
+++ b/src/collective/feedback/tests/test_feedbacks_get.py
@@ -0,0 +1,233 @@
+# -*- 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
+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 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)
+
+ 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/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..757c8b4
--- /dev/null
+++ b/src/collective/feedback/upgrades.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+from plone import api
+from plone.app.upgrade.utils import installOrReinstallProduct
+
+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")
+ 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"]
diff --git a/src/collective/feedback/upgrades.zcml b/src/collective/feedback/upgrades.zcml
new file mode 100644
index 0000000..cc7e2c1
--- /dev/null
+++ b/src/collective/feedback/upgrades.zcml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/test_plone60.cfg b/test_plone60.cfg
index 7dc9271..12e1d6f 100644
--- a/test_plone60.cfg
+++ b/test_plone60.cfg
@@ -54,3 +54,41 @@ 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
+
+# Added by buildout at 2024-02-08 21:49:12.973900
+zpretty = 3.1.0