diff --git a/.gitignore b/.gitignore
index 3988bfb..481d0ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,4 @@ report.html
reports/
venv/
# excludes
+.python-version
diff --git a/CHANGES.rst b/CHANGES.rst
index 701e29b..1d3dcc6 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,7 +5,11 @@ Changelog
1.1.4 (unreleased)
------------------
-- Nothing changed yet.
+- Add feedback update endpoint.
+ [folix-01]
+
+- Add read field to the comment.
+ [folix-01]
1.1.3 (2024-04-29)
diff --git a/src/collective/feedback/__init__.py b/src/collective/feedback/__init__.py
index b661503..43b0762 100644
--- a/src/collective/feedback/__init__.py
+++ b/src/collective/feedback/__init__.py
@@ -2,5 +2,4 @@
"""Init and utils."""
from zope.i18nmessageid import MessageFactory
-
_ = MessageFactory("collective.feedback")
diff --git a/src/collective/feedback/locales/update.py b/src/collective/feedback/locales/update.py
index ce89f54..9e6a067 100644
--- a/src/collective/feedback/locales/update.py
+++ b/src/collective/feedback/locales/update.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
import os
-import pkg_resources
import subprocess
+import pkg_resources
domain = "collective.feedback"
os.chdir(pkg_resources.resource_filename(domain, ""))
diff --git a/src/collective/feedback/permissions.zcml b/src/collective/feedback/permissions.zcml
index ee69ca2..1368e60 100644
--- a/src/collective/feedback/permissions.zcml
+++ b/src/collective/feedback/permissions.zcml
@@ -21,6 +21,10 @@
id="collective.feedback.ShowDeletedFeedbacks"
title="collective.feedback: Show Deleted Feedbacks"
/>
+
diff --git a/src/collective/feedback/profiles/default/metadata.xml b/src/collective/feedback/profiles/default/metadata.xml
index 498df83..6e1e374 100644
--- a/src/collective/feedback/profiles/default/metadata.xml
+++ b/src/collective/feedback/profiles/default/metadata.xml
@@ -1,6 +1,6 @@
- 1201
+ 1202
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 cfdb62b..62639c6 100644
--- a/src/collective/feedback/profiles/default/rolemap.xml
+++ b/src/collective/feedback/profiles/default/rolemap.xml
@@ -25,6 +25,11 @@
-
+
+
+
+
+
+
diff --git a/src/collective/feedback/restapi/services/add.py b/src/collective/feedback/restapi/services/add.py
index 77e418c..2b8151e 100644
--- a/src/collective/feedback/restapi/services/add.py
+++ b/src/collective/feedback/restapi/services/add.py
@@ -1,4 +1,3 @@
-from collective.feedback.interfaces import ICollectiveFeedbackStore
from plone import api
from plone.protect.interfaces import IDisableCSRFProtection
from plone.restapi.deserializer import json_body
@@ -7,6 +6,8 @@
from zope.component import getUtility
from zope.interface import alsoProvides
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+
class FeedbackAdd(Service):
"""
diff --git a/src/collective/feedback/restapi/services/configure.zcml b/src/collective/feedback/restapi/services/configure.zcml
index c80e9b3..402ba97 100644
--- a/src/collective/feedback/restapi/services/configure.zcml
+++ b/src/collective/feedback/restapi/services/configure.zcml
@@ -37,5 +37,13 @@
layer="collective.feedback.interfaces.ICollectiveFeedbackLayer"
name="@feedback-delete"
/>
+
diff --git a/src/collective/feedback/restapi/services/delete.py b/src/collective/feedback/restapi/services/delete.py
index 36fd8ff..9afed5f 100644
--- a/src/collective/feedback/restapi/services/delete.py
+++ b/src/collective/feedback/restapi/services/delete.py
@@ -1,12 +1,12 @@
-from collective.feedback.interfaces import ICollectiveFeedbackStore
from plone.protect.interfaces import IDisableCSRFProtection
from plone.restapi.services import Service
from zExceptions import BadRequest
from zope.component import getUtility
-from zope.interface import alsoProvides
-from zope.interface import implementer
+from zope.interface import alsoProvides, implementer
from zope.publisher.interfaces import IPublishTraverse
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+
@implementer(IPublishTraverse)
class FeedbackDelete(Service):
diff --git a/src/collective/feedback/restapi/services/get.py b/src/collective/feedback/restapi/services/get.py
index 41d7ccc..64454e5 100644
--- a/src/collective/feedback/restapi/services/get.py
+++ b/src/collective/feedback/restapi/services/get.py
@@ -1,7 +1,8 @@
-from AccessControl import Unauthorized
-from collective.feedback.interfaces import ICollectiveFeedbackStore
+import csv
from copy import deepcopy
from datetime import datetime
+
+from AccessControl import Unauthorized
from plone import api
from plone.restapi.batching import HypermediaBatch
from plone.restapi.search.utils import unflatten_dotted_dict
@@ -12,7 +13,7 @@
from zope.interface import implementer
from zope.publisher.interfaces import IPublishTraverse
-import csv
+from collective.feedback.interfaces import ICollectiveFeedbackStore
@implementer(IPublishTraverse)
@@ -115,6 +116,8 @@ def get_single_object_feedbacks(self, uid):
"answer": record._attrs.get("answer", ""),
"comment": record._attrs.get("comment", ""),
"title": commented_object.title,
+ "id": record.intid,
+ "read": record._attrs.get("read", ""),
}
)
@@ -135,6 +138,7 @@ 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(uid=uid)
if not obj and not api.user.has_permission(
@@ -161,6 +165,11 @@ def get_data(self):
data["vote_num"] += 1
data["vote_sum"] += vote
+ # Sign if page has unread comments
+ data["has_unread"] = data.get(
+ "has_unread", False
+ ) or not feedback._attrs.get("read", False)
+
# number of comment
comment = feedback._attrs.get("comment", "")
answer = feedback._attrs.get("answer", "")
@@ -174,10 +183,27 @@ def get_data(self):
if data["last_vote"] < date:
data["last_vote"] = date
- # avg calculation
+ pages_to_remove = []
+
+ has_undread = query.get("has_unread", None)
+
+ if has_undread in ("true", "false"):
+ has_undread = not (has_undread == "false") and has_undread == "true"
+ else:
+ has_undread = None
+
for uid, feedback in feedbacks.items():
+ # avg calculation
feedback["vote"] = feedback.pop("vote_sum") / feedback.pop("vote_num")
+ # Use has_unread filter
+ if has_undread is not None:
+ if feedback["has_unread"] != has_undread:
+ pages_to_remove.append(uid)
+
+ for uid in pages_to_remove:
+ del feedbacks[uid]
+
result = list(feedbacks.values())
# sort
diff --git a/src/collective/feedback/restapi/services/update.py b/src/collective/feedback/restapi/services/update.py
new file mode 100644
index 0000000..ce09740
--- /dev/null
+++ b/src/collective/feedback/restapi/services/update.py
@@ -0,0 +1,82 @@
+from plone.protect.interfaces import IDisableCSRFProtection
+from plone.restapi.deserializer import json_body
+from plone.restapi.services import Service
+from zExceptions import BadRequest, NotFound
+from zope.component import getUtility
+from zope.interface import alsoProvides, implementer
+from zope.publisher.interfaces import IPublishTraverse
+
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+
+
+@implementer(IPublishTraverse)
+class FeedbackUpdate(Service):
+ """
+ Service for update feedback to object, you can only update `read` field
+ """
+
+ def __init__(self, context, request):
+ super().__init__(context, request)
+ self.params = []
+
+ def publishTraverse(self, request, id):
+ # Consume any path segments after /@users as parameters
+ self.params.append(id)
+ return self
+
+ def reply(self):
+ alsoProvides(self.request, IDisableCSRFProtection)
+
+ tool = getUtility(ICollectiveFeedbackStore)
+
+ if self.params:
+ try:
+ id = int(self.params[0])
+ except ValueError:
+ raise BadRequest(f"Bad id={self.params[0]} format provided")
+
+ comment = tool.get(id)
+
+ if comment.get("error", "") == "NotFound":
+ raise NotFound()
+
+ form_data = json_body(self.request)
+
+ self.validate_form(form_data=form_data)
+
+ form_data = self.extract_data(form_data)
+
+ try:
+ res = tool.update(id, form_data)
+ except ValueError as e:
+ self.request.response.setStatus(500)
+ return dict(
+ error=dict(
+ type="InternalServerError",
+ message=getattr(e, "message", e.__str__()),
+ )
+ )
+
+ if res is None:
+ return self.reply_no_content()
+
+ self.request.response.setStatus(500)
+
+ return dict(
+ error=dict(
+ type="InternalServerError",
+ message="Unable to add. Contact site manager.",
+ )
+ )
+
+ def extract_data(sefl, form_data):
+ return {"read": form_data.get("read")}
+
+ def validate_form(self, form_data):
+ """
+ check all required fields and parameters
+ """
+ for field in ["read"]:
+ value = form_data.get(field, None)
+ if value is None:
+ raise BadRequest("Campo obbligatorio mancante: {}".format(field))
diff --git a/src/collective/feedback/storage/catalog.py b/src/collective/feedback/storage/catalog.py
index e2f6b9a..4266051 100644
--- a/src/collective/feedback/storage/catalog.py
+++ b/src/collective/feedback/storage/catalog.py
@@ -3,8 +3,7 @@
from repoze.catalog.indexes.field import CatalogFieldIndex
from repoze.catalog.indexes.text import CatalogTextIndex
from souper.interfaces import ICatalogFactory
-from souper.soup import NodeAttributeIndexer
-from souper.soup import NodeTextIndexer
+from souper.soup import NodeAttributeIndexer, NodeTextIndexer
from zope.interface import implementer
diff --git a/src/collective/feedback/storage/store.py b/src/collective/feedback/storage/store.py
index dd0805b..b795ea4 100644
--- a/src/collective/feedback/storage/store.py
+++ b/src/collective/feedback/storage/store.py
@@ -1,18 +1,14 @@
# -*- coding: utf-8 -*-
-from collective.feedback.interfaces import ICollectiveFeedbackStore
+import logging
from datetime import datetime
-from plone import api
-from repoze.catalog.query import And
-from repoze.catalog.query import Any
-from repoze.catalog.query import Contains
-from repoze.catalog.query import Eq
-from souper.soup import get_soup
-from souper.soup import Record
-from zope.interface import implementer
-import logging
import six
+from plone import api
+from repoze.catalog.query import And, Any, Contains, Eq
+from souper.soup import Record, get_soup
+from zope.interface import implementer
+from collective.feedback.interfaces import ICollectiveFeedbackStore
logger = logging.getLogger(__name__)
@@ -21,7 +17,7 @@
class CollectiveFeedbackStore(object):
"""Store class for collective.feedback catalog soup"""
- fields = ["uid", "url", "title", "comment", "vote", "answer", "date"]
+ fields = ["uid", "url", "title", "comment", "vote", "answer", "date", "read"]
text_index = "title"
indexes = ["title", "vote", "uid"]
keyword_indexes = []
@@ -83,10 +79,10 @@ def parse_query_params(self, index, value):
# return "{} in any('{}')".format(index, value)
else:
return Eq(index, value)
- if isinstance(value, int):
- return "{} == {}".format(index, value)
- else:
- return "{} == '{}'".format(index, value)
+ # if isinstance(value, int):
+ # return "{} == {}".format(index, value)
+ # else:
+ # return "{} == '{}'".format(index, value)
def get_record(self, id):
if isinstance(id, six.text_type) or isinstance(id, str):
@@ -113,6 +109,7 @@ def update(self, id, data):
else:
record.attrs[k] = v
+
self.soup.reindex(records=[record])
def delete(self, id):
@@ -123,5 +120,12 @@ def delete(self, id):
return {"error": "NotFound"}
del self.soup[record]
+ def get(self, id):
+ try:
+ return self.soup.get(id)
+ except KeyError:
+ logger.error('[GET] Subscription with id "{}" not found.'.format(id))
+ return {"error": "NotFound"}
+
def clear(self):
self.soup.clear()
diff --git a/src/collective/feedback/testing.py b/src/collective/feedback/testing.py
index a04d653..df70938 100644
--- a/src/collective/feedback/testing.py
+++ b/src/collective/feedback/testing.py
@@ -1,16 +1,17 @@
-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
-
-import collective.feedback
import collective.honeypot
import collective.honeypot.config
import plone.restapi
import souper.plone
+from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
+from plone.app.testing import (
+ FunctionalTesting,
+ IntegrationTesting,
+ PloneSandboxLayer,
+ applyProfile,
+)
+from plone.testing.zope import WSGI_SERVER_FIXTURE
+import collective.feedback
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 a17a00c..d41169e 100644
--- a/src/collective/feedback/tests/test_delete_content.py
+++ b/src/collective/feedback/tests/test_delete_content.py
@@ -1,19 +1,21 @@
# -*- coding: utf-8 -*-
-from collective.feedback.interfaces import ICollectiveFeedbackStore
-from collective.feedback.testing import RESTAPI_TESTING
+import unittest
from datetime import datetime
+
+import transaction
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.app.testing import (
+ SITE_OWNER_NAME,
+ SITE_OWNER_PASSWORD,
+ TEST_USER_ID,
+ setRoles,
+)
from plone.restapi.testing import RelativeSession
-from souper.soup import get_soup
-from souper.soup import Record
+from souper.soup import Record, get_soup
from zope.component import getUtility
-import transaction
-import unittest
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+from collective.feedback.testing import RESTAPI_TESTING
class TestCustomerSatisfactionGet(unittest.TestCase):
diff --git a/src/collective/feedback/tests/test_feedbacks_add.py b/src/collective/feedback/tests/test_feedbacks_add.py
index 5d20231..08f9b3c 100644
--- a/src/collective/feedback/tests/test_feedbacks_add.py
+++ b/src/collective/feedback/tests/test_feedbacks_add.py
@@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
-from collective.feedback.interfaces import ICollectiveFeedbackStore
-from collective.feedback.testing import RESTAPI_TESTING
+import unittest
+
+import transaction
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.app.testing import (
+ SITE_OWNER_NAME,
+ SITE_OWNER_PASSWORD,
+ TEST_USER_ID,
+ setRoles,
+)
from plone.restapi.testing import RelativeSession
from zope.component import getUtility
-import transaction
-import unittest
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+from collective.feedback.testing import RESTAPI_TESTING
class TestAdd(unittest.TestCase):
diff --git a/src/collective/feedback/tests/test_feedbacks_get.py b/src/collective/feedback/tests/test_feedbacks_get.py
index 6f413e0..224c43a 100644
--- a/src/collective/feedback/tests/test_feedbacks_get.py
+++ b/src/collective/feedback/tests/test_feedbacks_get.py
@@ -1,30 +1,35 @@
# -*- coding: utf-8 -*-
-from collective.feedback.interfaces import ICollectiveFeedbackStore
-from collective.feedback.testing import RESTAPI_TESTING
+import unittest
from datetime import datetime
+
+import transaction
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.app.testing import (
+ SITE_OWNER_NAME,
+ SITE_OWNER_PASSWORD,
+ TEST_USER_ID,
+ setRoles,
+)
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 souper.soup import Record, get_soup
from zope.component import getUtility
-import transaction
-import unittest
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+from collective.feedback.testing import RESTAPI_TESTING
class TestGet(unittest.TestCase):
layer = RESTAPI_TESTING
- def add_record(self, date=None, vote="", uid="", comment="", title=""):
+ def add_record(self, date=None, vote="", uid="", comment="", title="", read=False):
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
@@ -35,6 +40,9 @@ def add_record(self, date=None, vote="", uid="", comment="", title=""):
record.attrs["uid"] = uid
if title:
record.attrs["title"] = title
+ if read:
+ record.attrs["read"] = read
+
soup.add(record)
transaction.commit()
@@ -124,6 +132,7 @@ def test_endpoint_returns_data(self):
[
{
"comments": 1,
+ "has_unread": True,
"last_vote": json_compatible(now),
"title": "",
"uid": "",
@@ -231,3 +240,59 @@ def test_users_without_permission_dont_have_can_delete_feedbacks_action(self):
self.assertIn("can_delete_feedbacks", res["actions"])
self.assertFalse(res["actions"]["can_delete_feedbacks"])
+
+ def test_has_unread_filter(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, read=True)
+
+ 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,
+ "has_unread": False,
+ }
+ ],
+ )
+
+ self.add_record(vote=1, comment="is ok", date=now, read=False)
+
+ # has_unread=true case
+ response = self.api_session.get(self.url + "?has_unread=true")
+
+ self.assertEqual(
+ response.json()["items"],
+ [
+ {
+ "comments": 2,
+ "last_vote": json_compatible(now),
+ "title": "",
+ "uid": "",
+ "vote": 1.0,
+ "has_unread": True,
+ }
+ ],
+ )
+
+ self.add_record(vote=1, comment="is ok", date=now)
+
+ # has_unread=false case
+ response = self.api_session.get(self.url + "?has_unread=false")
+
+ self.assertEqual(response.json()["items"], [])
diff --git a/src/collective/feedback/tests/test_store.py b/src/collective/feedback/tests/test_store.py
index 1ffaffa..3a09aa1 100644
--- a/src/collective/feedback/tests/test_store.py
+++ b/src/collective/feedback/tests/test_store.py
@@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
-from collective.feedback.interfaces import ICollectiveFeedbackStore
-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
+import unittest
import transaction
-import unittest
+from plone.app.testing import TEST_USER_ID, setRoles
+from zope.component import getUtility
+
+from collective.feedback.interfaces import ICollectiveFeedbackStore
+from collective.feedback.testing import FUNCTIONAL_TESTING
class TestTool(unittest.TestCase):
diff --git a/src/collective/feedback/upgrades.py b/src/collective/feedback/upgrades.py
index 0f42610..f06ae53 100644
--- a/src/collective/feedback/upgrades.py
+++ b/src/collective/feedback/upgrades.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
-from plone import api
-from plone.app.upgrade.utils import installOrReinstallProduct
-
import logging
+from plone import api
+from plone.app.upgrade.utils import installOrReinstallProduct
logger = logging.getLogger(__name__)
diff --git a/src/collective/feedback/upgrades.zcml b/src/collective/feedback/upgrades.zcml
index bb953d3..4b23689 100644
--- a/src/collective/feedback/upgrades.zcml
+++ b/src/collective/feedback/upgrades.zcml
@@ -37,4 +37,14 @@
handler=".upgrades.update_actions"
/>
+
+
+