Skip to content

Commit

Permalink
Merge branch 'master' into file-handling-use-cases
Browse files Browse the repository at this point in the history
  • Loading branch information
tisto committed Nov 13, 2017
2 parents 5d5e173 + 0004585 commit bb22ab7
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 72 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Expand Up @@ -10,6 +10,10 @@ Changelog
existing value.
[sneridagh]

- Add 'is_editable' and 'is_deletable' to the serialization of comments objects.
Also refactored the comments endpoint to DRY.
[sneridagh]


1.0a23 (2017-11-07)
-------------------
Expand Down
4 changes: 4 additions & 0 deletions docs/source/_json/comments_get.resp
Expand Up @@ -13,6 +13,8 @@ Content-Type: application/json
"comment_id": "1477076400000000",
"creation_date": "2016-10-21T19:00:00",
"in_reply_to": null,
"is_deletable": true,
"is_editable": true,
"modification_date": "2016-10-21T19:00:00",
"text": {
"data": "Comment 1",
Expand All @@ -29,6 +31,8 @@ Content-Type: application/json
"comment_id": "1477076400000001",
"creation_date": "2016-10-21T19:00:00",
"in_reply_to": "1477076400000000",
"is_deletable": true,
"is_editable": true,
"modification_date": "2016-10-21T19:00:00",
"text": {
"data": "Comment 1.1",
Expand Down
20 changes: 16 additions & 4 deletions src/plone/restapi/serializer/discussion.py
@@ -1,12 +1,18 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IConversation
from plone.restapi.batching import HypermediaBatch
from zope.publisher.interfaces import IRequest
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.interfaces import IJsonCompatible
from zope.component import adapter, getMultiAdapter
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.services.discussion.utils import can_delete
from plone.restapi.services.discussion.utils import can_delete_own
from plone.restapi.services.discussion.utils import can_edit
from plone.restapi.services.discussion.utils import delete_own_comment_allowed
from plone.restapi.services.discussion.utils import edit_comment_allowed
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.interface import implementer
from zope.publisher.interfaces import IRequest


@implementer(ISerializeToJson)
Expand Down Expand Up @@ -61,6 +67,10 @@ def __call__(self):
else:
parent_url = None
in_reply_to = None

doc_allowed = delete_own_comment_allowed()
delete_own = doc_allowed and can_delete_own(self.context)

return {
'@id': url,
'@type': self.context.portal_type,
Expand All @@ -76,4 +86,6 @@ def __call__(self):
'author_name': self.context.author_name,
'creation_date': IJsonCompatible(self.context.creation_date),
'modification_date': IJsonCompatible(self.context.modification_date), # noqa
'is_editable': edit_comment_allowed() and can_edit(self.context),
'is_deletable': can_delete(self.context) or delete_own
}
77 changes: 9 additions & 68 deletions src/plone/restapi/services/discussion/conversation.py
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
from AccessControl import getSecurityManager
from Acquisition import aq_inner
from plone.app.discussion.browser.comment import EditCommentForm
from plone.app.discussion.browser.comments import CommentForm
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.registry.interfaces import IRegistry
from plone.restapi.deserializer import json_body
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.services import Service
from plone.restapi.services.discussion.utils import can_delete
from plone.restapi.services.discussion.utils import can_delete_own
from plone.restapi.services.discussion.utils import can_edit
from plone.restapi.services.discussion.utils import delete_own_comment_allowed
from plone.restapi.services.discussion.utils import edit_comment_allowed
from zExceptions import BadRequest
from zExceptions import Unauthorized
from zope.component import getMultiAdapter
Expand All @@ -22,11 +23,6 @@
import plone.protect.interfaces


def permission_exists(permission_id):
permission = queryUtility(IPermission, permission_id)
return permission is not None


def fix_location_header(context, request):
# This replaces the location header as sent by p.a.discussion's forms with
# a RESTapi compatible location.
Expand Down Expand Up @@ -123,7 +119,7 @@ def reply(self):
comment = conversation[self.comment_id]

# Permission checks
if not (self.edit_comment_allowed() and self.can_edit(comment)):
if not (edit_comment_allowed() and can_edit(comment)):
raise Unauthorized()

# Fake request data
Expand All @@ -146,20 +142,6 @@ def reply(self):
self.request.response.setStatus(204)
fix_location_header(self.context, self.request)

def edit_comment_allowed(self):
# Check if editing comments is allowed in the registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.edit_comment_enabled

def can_edit(self, reply):
"""Returns true if current user has the 'Edit comments'
permission.
"""
return getSecurityManager().checkPermission(
'Edit comments', aq_inner(reply)
)


@implementer(IPublishTraverse)
class CommentsDelete(Service):
Expand All @@ -181,10 +163,9 @@ def reply(self):
comment = conversation[self.comment_id]

# Permission checks
can_delete = self.can_delete(comment)
doc_allowed = self.delete_own_comment_allowed()
delete_own = doc_allowed and self.can_delete_own(comment)
if not (can_delete or delete_own):
doc_allowed = delete_own_comment_allowed()
delete_own = doc_allowed and can_delete_own(comment)
if not (can_delete(comment) or delete_own):
raise Unauthorized()

del conversation[self.comment_id]
Expand All @@ -197,43 +178,3 @@ def _has_permission(self, permission_id):
# which case we need to check
permission = queryUtility(IPermission, permission_id)
return permission is not None

def can_review(self):
"""Returns true if current user has the 'Review comments' permission.
"""
return getSecurityManager().checkPermission('Review comments',
aq_inner(self.context))

def can_delete(self, reply):
"""Returns true if current user has the 'Delete comments'
permission.
"""
if not permission_exists('plone.app.discussion.DeleteComments'):
# Older versions of p.a.discussion do not support this yet.
return self.can_review()

return getSecurityManager().checkPermission(
'Delete comments', aq_inner(reply)
)

def delete_own_comment_allowed(self):
if not permission_exists('plone.app.discussion.DeleteOwnComments'):
# Older versions of p.a.discussion do not support this yet.
return False
# Check if delete own comments is allowed in the registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.delete_own_comment_enabled

def can_delete_own(self, comment):
"""Returns true if the current user could delete the comment if it had
no replies. This is used to prepare hidden form buttons for JS.
"""
if not permission_exists('plone.app.discussion.DeleteOwnComments'):
# Older versions of p.a.discussion do not support this yet.
return False
try:
return comment.restrictedTraverse(
'@@delete-own-comment').could_delete()
except Unauthorized:
return False
74 changes: 74 additions & 0 deletions src/plone/restapi/services/discussion/utils.py
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
from AccessControl import getSecurityManager
from Acquisition import aq_inner
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.registry.interfaces import IRegistry
from zExceptions import Unauthorized
from zope.component import queryUtility
from zope.security.interfaces import IPermission


def edit_comment_allowed():
# Check if editing comments is allowed in the registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.edit_comment_enabled


def can_edit(comment):
"""Returns true if current user has the 'Edit comments'
permission.
"""
return bool(getSecurityManager().checkPermission(
'Edit comments', aq_inner(comment)
))


def permission_exists(permission_id):
permission = queryUtility(IPermission, permission_id)
return permission is not None


def can_review(comment):
"""Returns true if current user has the 'Review comments' permission.
"""
return bool(getSecurityManager().checkPermission(
'Review comments', aq_inner(comment)
))


def can_delete(comment):
"""Returns true if current user has the 'Delete comments'
permission.
"""
if not permission_exists('plone.app.discussion.DeleteComments'):
# Older versions of p.a.discussion do not support this yet.
return can_review(comment)

return bool(getSecurityManager().checkPermission(
'Delete comments', aq_inner(comment)
))


def delete_own_comment_allowed():
if not permission_exists('plone.app.discussion.DeleteOwnComments'):
# Older versions of p.a.discussion do not support this yet.
return False
# Check if delete own comments is allowed in the registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.delete_own_comment_enabled


def can_delete_own(comment):
"""Returns true if the current user could delete the comment if it had
no replies. This is used to prepare hidden form buttons for JS.
"""
if not permission_exists('plone.app.discussion.DeleteOwnComments'):
# Older versions of p.a.discussion do not support this yet.
return False
try:
return comment.restrictedTraverse(
'@@delete-own-comment').could_delete()
except Unauthorized:
return False
2 changes: 2 additions & 0 deletions src/plone/restapi/tests/test_comments.py
Expand Up @@ -91,6 +91,8 @@ def test_comment(self):
'author_name',
'creation_date',
'modification_date',
'is_editable',
'is_deletable'
]
self.assertEqual(
set(output.keys()),
Expand Down
8 changes: 8 additions & 0 deletions src/plone/restapi/tests/test_services_comments.py
Expand Up @@ -148,11 +148,15 @@ def test_permissions_delete_comment(self):
response = self.api_session.get(url)
comment_url = response.json()['items'][0]['@id']
self.assertFalse(comment_url.endswith('@comments'))
self.assertTrue(response.json()['items'][0]['is_deletable'])

# Other user may not delete this
response = self.user_session.delete(comment_url)
self.assertEqual(401, response.status_code)

response = self.user_session.get(url)
self.assertFalse(response.json()['items'][0]['is_deletable'])

# The owner may
response = self.api_session.delete(comment_url)
self.assertEqual(204, response.status_code)
Expand All @@ -166,11 +170,15 @@ def test_permissions_update_comment(self):
response = self.api_session.get(url)
comment_url = response.json()['items'][0]['@id']
self.assertFalse(comment_url.endswith('@comments'))
self.assertTrue(response.json()['items'][0]['is_editable'])

# Other user may not update this
response = self.user_session.patch(comment_url, json={'text': 'new'})
self.assertEqual(401, response.status_code)

response = self.user_session.get(url)
self.assertFalse(response.json()['items'][0]['is_editable'])

# The owner may
response = self.api_session.patch(comment_url, json={'text': 'new'})
self.assertEqual(204, response.status_code)

0 comments on commit bb22ab7

Please sign in to comment.