Commit 7de58823 by wajeeha-khalid

MA-860; No moderator permissions on closed thread

parent 9fcc1c32
...@@ -601,6 +601,10 @@ def create_comment(request, comment_data): ...@@ -601,6 +601,10 @@ def create_comment(request, comment_data):
except Http404: except Http404:
raise ValidationError({"thread_id": ["Invalid value."]}) raise ValidationError({"thread_id": ["Invalid value."]})
# if a thread is closed; no new comments could be made to it
if cc_thread['closed']:
raise PermissionDenied
_check_initializable_comment_fields(comment_data, context) _check_initializable_comment_fields(comment_data, context)
serializer = CommentSerializer(data=comment_data, context=context) serializer = CommentSerializer(data=comment_data, context=context)
actions_form = CommentActionsForm(comment_data) actions_form = CommentActionsForm(comment_data)
......
...@@ -56,8 +56,16 @@ def get_editable_fields(cc_content, context): ...@@ -56,8 +56,16 @@ def get_editable_fields(cc_content, context):
""" """
Return the set of fields that the requester can edit on the given content Return the set of fields that the requester can edit on the given content
""" """
# no edits, except 'abuse_flagged' are allowed on closed threads
ret = {"abuse_flagged"}
if (cc_content["type"] == "thread" and cc_content["closed"]) or (
cc_content["type"] == "comment" and context["thread"]["closed"]
):
return ret
# Shared fields # Shared fields
ret = {"abuse_flagged", "voted"} ret |= {"voted"}
if _is_author_or_privileged(cc_content, context): if _is_author_or_privileged(cc_content, context):
ret |= {"raw_body"} ret |= {"raw_body"}
......
...@@ -614,7 +614,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto ...@@ -614,7 +614,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto
"title": "Another Test Title", "title": "Another Test Title",
"body": "More content", "body": "More content",
"pinned": False, "pinned": False,
"closed": True, "closed": False,
"abuse_flaggers": [], "abuse_flaggers": [],
"votes": {"up_count": 9}, "votes": {"up_count": 9},
"comments_count": 18, "comments_count": 18,
...@@ -668,7 +668,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto ...@@ -668,7 +668,7 @@ class GetThreadListTest(CommentsServiceMockMixin, UrlResetMixin, SharedModuleSto
"raw_body": "More content", "raw_body": "More content",
"rendered_body": "<p>More content</p>", "rendered_body": "<p>More content</p>",
"pinned": False, "pinned": False,
"closed": True, "closed": False,
"following": False, "following": False,
"abuse_flagged": False, "abuse_flagged": False,
"voted": False, "voted": False,
......
...@@ -63,6 +63,41 @@ class DiscussionAPIViewTestMixin(CommentsServiceMockMixin, UrlResetMixin): ...@@ -63,6 +63,41 @@ class DiscussionAPIViewTestMixin(CommentsServiceMockMixin, UrlResetMixin):
parsed_content = json.loads(response.content) parsed_content = json.loads(response.content)
self.assertEqual(parsed_content, expected_content) self.assertEqual(parsed_content, expected_content)
def register_thread(self, overrides=None):
"""
Create cs_thread with minimal fields and register response
"""
cs_thread = make_minimal_cs_thread({
"id": "test_thread",
"course_id": unicode(self.course.id),
"commentable_id": "original_topic",
"username": self.user.username,
"user_id": str(self.user.id),
"thread_type": "discussion",
"title": "Original Title",
"body": "Original body",
})
cs_thread.update(overrides or {})
self.register_get_thread_response(cs_thread)
self.register_put_thread_response(cs_thread)
def register_comment(self, overrides=None):
"""
Create cs_comment with minimal fields and register response
"""
cs_comment = make_minimal_cs_comment({
"id": "test_comment",
"course_id": unicode(self.course.id),
"thread_id": "test_thread",
"username": self.user.username,
"user_id": str(self.user.id),
"body": "Original body",
})
cs_comment.update(overrides or {})
self.register_get_comment_response(cs_comment)
self.register_put_comment_response(cs_comment)
self.register_post_comment_response(cs_comment, thread_id="test_thread")
def test_not_authenticated(self): def test_not_authenticated(self):
self.client.logout() self.client.logout()
response = self.client.get(self.url) response = self.client.get(self.url)
...@@ -534,6 +569,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -534,6 +569,7 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
self.assertEqual(response_data, expected_response_data) self.assertEqual(response_data, expected_response_data)
@ddt.ddt
@httpretty.activate @httpretty.activate
@disable_signal(api, 'thread_edited') @disable_signal(api, 'thread_edited')
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
...@@ -544,24 +580,11 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest ...@@ -544,24 +580,11 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
super(ThreadViewSetPartialUpdateTest, self).setUp() super(ThreadViewSetPartialUpdateTest, self).setUp()
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"}) self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
def test_basic(self): def expected_response_data(self, overrides=None):
self.register_get_user_response(self.user) """
cs_thread = make_minimal_cs_thread({ create expected response data from comment update endpoint
"id": "test_thread", """
"course_id": unicode(self.course.id), response_data = {
"commentable_id": "original_topic",
"username": self.user.username,
"user_id": str(self.user.id),
"created_at": "2015-05-29T00:00:00Z",
"updated_at": "2015-05-29T00:00:00Z",
"thread_type": "discussion",
"title": "Original Title",
"body": "Original body",
})
self.register_get_thread_response(cs_thread)
self.register_put_thread_response(cs_thread)
request_data = {"raw_body": "Edited body"}
expected_response_data = {
"id": "test_thread", "id": "test_thread",
"course_id": unicode(self.course.id), "course_id": unicode(self.course.id),
"topic_id": "original_topic", "topic_id": "original_topic",
...@@ -569,12 +592,12 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest ...@@ -569,12 +592,12 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
"group_name": None, "group_name": None,
"author": self.user.username, "author": self.user.username,
"author_label": None, "author_label": None,
"created_at": "2015-05-29T00:00:00Z", "created_at": "1970-01-01T00:00:00Z",
"updated_at": "2015-05-29T00:00:00Z", "updated_at": "1970-01-01T00:00:00Z",
"type": "discussion", "type": "discussion",
"title": "Original Title", "title": "Original Title",
"raw_body": "Edited body", "raw_body": "Original body",
"rendered_body": "<p>Edited body</p>", "rendered_body": "<p>Original body</p>",
"pinned": False, "pinned": False,
"closed": False, "closed": False,
"following": False, "following": False,
...@@ -586,19 +609,31 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest ...@@ -586,19 +609,31 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread", "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
"endorsed_comment_list_url": None, "endorsed_comment_list_url": None,
"non_endorsed_comment_list_url": None, "non_endorsed_comment_list_url": None,
"editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"], "editable_fields": [],
"read": False, "read": False,
"has_endorsed": False, "has_endorsed": False,
"response_count": 0, "response_count": 0,
} }
response = self.client.patch( # pylint: disable=no-member response_data.update(overrides or {})
self.url, return response_data
json.dumps(request_data),
content_type="application/merge-patch+json" def test_basic(self):
) self.register_get_user_response(self.user)
self.register_thread({"created_at": "Test Date", "updated_at": "Test Date"})
request_data = {"raw_body": "Edited body"}
response = self.request_patch(request_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data, expected_response_data) self.assertEqual(
response_data,
self.expected_response_data({
"raw_body": "Edited body",
"rendered_body": "<p>Edited body</p>",
"editable_fields": ["abuse_flagged", "following", "raw_body", "title", "topic_id", "type", "voted"],
"created_at": "Test Date",
"updated_at": "Test Date",
})
)
self.assertEqual( self.assertEqual(
httpretty.last_request().parsed_body, httpretty.last_request().parsed_body,
{ {
...@@ -617,18 +652,9 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest ...@@ -617,18 +652,9 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
def test_error(self): def test_error(self):
self.register_get_user_response(self.user) self.register_get_user_response(self.user)
cs_thread = make_minimal_cs_thread({ self.register_thread()
"id": "test_thread",
"course_id": unicode(self.course.id),
"user_id": str(self.user.id),
})
self.register_get_thread_response(cs_thread)
request_data = {"title": ""} request_data = {"title": ""}
response = self.client.patch( # pylint: disable=no-member response = self.request_patch(request_data)
self.url,
json.dumps(request_data),
content_type="application/merge-patch+json"
)
expected_response_data = { expected_response_data = {
"field_errors": {"title": {"developer_message": "This field may not be blank."}} "field_errors": {"title": {"developer_message": "This field may not be blank."}}
} }
...@@ -636,6 +662,42 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest ...@@ -636,6 +662,42 @@ class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTest
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data, expected_response_data) self.assertEqual(response_data, expected_response_data)
@ddt.data(
("abuse_flagged", True),
("abuse_flagged", False),
)
@ddt.unpack
def test_closed_thread(self, field, value):
self.register_get_user_response(self.user)
self.register_thread({"closed": True})
self.register_flag_response("thread", "test_thread")
request_data = {field: value}
response = self.request_patch(request_data)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(
response_data,
self.expected_response_data({
"closed": True,
"abuse_flagged": value,
"editable_fields": ["abuse_flagged"],
})
)
@ddt.data(
("raw_body", "Edited body"),
("voted", True),
("following", True),
)
@ddt.unpack
def test_closed_thread_error(self, field, value):
self.register_get_user_response(self.user)
self.register_thread({"closed": True})
self.register_flag_response("thread", "test_thread")
request_data = {field: value}
response = self.request_patch(request_data)
self.assertEqual(response.status_code, 400)
@httpretty.activate @httpretty.activate
@disable_signal(api, 'thread_deleted') @disable_signal(api, 'thread_deleted')
...@@ -855,22 +917,8 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -855,22 +917,8 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
def test_basic(self): def test_basic(self):
self.register_get_user_response(self.user) self.register_get_user_response(self.user)
self.register_get_thread_response( self.register_thread()
make_minimal_cs_thread({ self.register_comment()
"id": "test_thread",
"course_id": unicode(self.course.id),
"commentable_id": "test_topic",
})
)
self.register_post_comment_response(
{
"id": "test_comment",
"username": self.user.username,
"created_at": "2015-05-27T00:00:00Z",
"updated_at": "2015-05-27T00:00:00Z",
},
thread_id="test_thread"
)
request_data = { request_data = {
"thread_id": "test_thread", "thread_id": "test_thread",
"raw_body": "Test body", "raw_body": "Test body",
...@@ -881,8 +929,8 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -881,8 +929,8 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
"parent_id": None, "parent_id": None,
"author": self.user.username, "author": self.user.username,
"author_label": None, "author_label": None,
"created_at": "2015-05-27T00:00:00Z", "created_at": "1970-01-01T00:00:00Z",
"updated_at": "2015-05-27T00:00:00Z", "updated_at": "1970-01-01T00:00:00Z",
"raw_body": "Test body", "raw_body": "Test body",
"rendered_body": "<p>Test body</p>", "rendered_body": "<p>Test body</p>",
"endorsed": False, "endorsed": False,
...@@ -929,7 +977,23 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase): ...@@ -929,7 +977,23 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data, expected_response_data) self.assertEqual(response_data, expected_response_data)
def test_closed_thread(self):
self.register_get_user_response(self.user)
self.register_thread({"closed": True})
self.register_comment()
request_data = {
"thread_id": "test_thread",
"raw_body": "Test body"
}
response = self.client.post(
self.url,
json.dumps(request_data),
content_type="application/json"
)
self.assertEqual(response.status_code, 403)
@ddt.ddt
@disable_signal(api, 'comment_edited') @disable_signal(api, 'comment_edited')
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin): class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
...@@ -942,36 +1006,21 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes ...@@ -942,36 +1006,21 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
self.addCleanup(httpretty.disable) self.addCleanup(httpretty.disable)
self.register_get_user_response(self.user) self.register_get_user_response(self.user)
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"}) self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
cs_thread = make_minimal_cs_thread({
"id": "test_thread",
"course_id": unicode(self.course.id),
})
self.register_get_thread_response(cs_thread)
cs_comment = make_minimal_cs_comment({
"id": "test_comment",
"course_id": cs_thread["course_id"],
"thread_id": cs_thread["id"],
"username": self.user.username,
"user_id": str(self.user.id),
"created_at": "2015-06-03T00:00:00Z",
"updated_at": "2015-06-03T00:00:00Z",
"body": "Original body",
})
self.register_get_comment_response(cs_comment)
self.register_put_comment_response(cs_comment)
def test_basic(self): def expected_response_data(self, overrides=None):
request_data = {"raw_body": "Edited body"} """
expected_response_data = { create expected response data from comment update endpoint
"""
response_data = {
"id": "test_comment", "id": "test_comment",
"thread_id": "test_thread", "thread_id": "test_thread",
"parent_id": None, "parent_id": None,
"author": self.user.username, "author": self.user.username,
"author_label": None, "author_label": None,
"created_at": "2015-06-03T00:00:00Z", "created_at": "1970-01-01T00:00:00Z",
"updated_at": "2015-06-03T00:00:00Z", "updated_at": "1970-01-01T00:00:00Z",
"raw_body": "Edited body", "raw_body": "Original body",
"rendered_body": "<p>Edited body</p>", "rendered_body": "<p>Original body</p>",
"endorsed": False, "endorsed": False,
"endorsed_by": None, "endorsed_by": None,
"endorsed_by_label": None, "endorsed_by_label": None,
...@@ -980,16 +1029,28 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes ...@@ -980,16 +1029,28 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
"voted": False, "voted": False,
"vote_count": 0, "vote_count": 0,
"children": [], "children": [],
"editable_fields": ["abuse_flagged", "raw_body", "voted"], "editable_fields": [],
} }
response = self.client.patch( # pylint: disable=no-member response_data.update(overrides or {})
self.url, return response_data
json.dumps(request_data),
content_type="application/merge-patch+json" def test_basic(self):
) self.register_thread()
self.register_comment({"created_at": "Test Date", "updated_at": "Test Date"})
request_data = {"raw_body": "Edited body"}
response = self.request_patch(request_data)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data, expected_response_data) self.assertEqual(
response_data,
self.expected_response_data({
"raw_body": "Edited body",
"rendered_body": "<p>Edited body</p>",
"editable_fields": ["abuse_flagged", "raw_body", "voted"],
"created_at": "Test Date",
"updated_at": "Test Date",
})
)
self.assertEqual( self.assertEqual(
httpretty.last_request().parsed_body, httpretty.last_request().parsed_body,
{ {
...@@ -1003,12 +1064,10 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes ...@@ -1003,12 +1064,10 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
) )
def test_error(self): def test_error(self):
self.register_thread()
self.register_comment()
request_data = {"raw_body": ""} request_data = {"raw_body": ""}
response = self.client.patch( # pylint: disable=no-member response = self.request_patch(request_data)
self.url,
json.dumps(request_data),
content_type="application/merge-patch+json"
)
expected_response_data = { expected_response_data = {
"field_errors": {"raw_body": {"developer_message": "This field may not be blank."}} "field_errors": {"raw_body": {"developer_message": "This field may not be blank."}}
} }
...@@ -1016,6 +1075,40 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes ...@@ -1016,6 +1075,40 @@ class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTes
response_data = json.loads(response.content) response_data = json.loads(response.content)
self.assertEqual(response_data, expected_response_data) self.assertEqual(response_data, expected_response_data)
@ddt.data(
("abuse_flagged", True),
("abuse_flagged", False),
)
@ddt.unpack
def test_closed_thread(self, field, value):
self.register_thread({"closed": True})
self.register_comment()
self.register_flag_response("comment", "test_comment")
request_data = {field: value}
response = self.request_patch(request_data)
self.assertEqual(response.status_code, 200)
response_data = json.loads(response.content)
self.assertEqual(
response_data,
self.expected_response_data({
"abuse_flagged": value,
"editable_fields": ["abuse_flagged"],
})
)
@ddt.data(
("raw_body", "Edited body"),
("voted", True),
("following", True),
)
@ddt.unpack
def test_closed_thread_error(self, field, value):
self.register_thread({"closed": True})
self.register_comment()
request_data = {field: value}
response = self.request_patch(request_data)
self.assertEqual(response.status_code, 400)
@httpretty.activate @httpretty.activate
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}) @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
......
...@@ -301,6 +301,16 @@ class CommentsServiceMockMixin(object): ...@@ -301,6 +301,16 @@ class CommentsServiceMockMixin(object):
""" """
self.assert_query_params_equal(httpretty.last_request(), expected_params) self.assert_query_params_equal(httpretty.last_request(), expected_params)
def request_patch(self, request_data):
"""
make a request to PATCH endpoint and return response
"""
return self.client.patch( # pylint: disable=no-member
self.url,
json.dumps(request_data),
content_type="application/merge-patch+json"
)
def make_minimal_cs_thread(overrides=None): def make_minimal_cs_thread(overrides=None):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment