diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee index 24970d6510..53904d9b87 100644 --- a/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee +++ b/common/static/coffee/spec/discussion/view/discussion_thread_view_spec.coffee @@ -387,3 +387,24 @@ describe "DiscussionThreadView", -> "Showing first 6 responses", "Load all responses" ) + + describe "post restrictions", -> + beforeEach -> + @thread.attributes.ability = _.extend(@thread.attributes.ability, { + can_report: false + can_vote: false + }) + @view = new DiscussionThreadView( + model: @thread + el: $("#fixture-element") + mode: "tab" + course_settings: DiscussionSpecHelper.makeCourseSettings() + ) + + it "doesn't show report option if can_report ability is disabled", -> + @view.render() + expect(@view.$el.find(".action-report").closest(".actions-item")).toHaveClass('is-hidden') + + it "doesn't show voting button if can_vote ability is disabled", -> + @view.render() + expect(@view.$el.find(".action-vote").closest(".actions-item")).toHaveClass('is-hidden') diff --git a/common/static/coffee/src/discussion/content.coffee b/common/static/coffee/src/discussion/content.coffee index 5c030fab01..49f199ab1c 100644 --- a/common/static/coffee/src/discussion/content.coffee +++ b/common/static/coffee/src/discussion/content.coffee @@ -11,6 +11,8 @@ if Backbone? can_reply: '.discussion-reply' can_delete: '.admin-delete' can_openclose: '.admin-openclose' + can_report: '.admin-report' + can_vote: '.admin-vote' urlMappers: {} @@ -97,7 +99,7 @@ if Backbone? pinned = @get("pinned") @set("pinned",pinned) @trigger "change", @ - + flagAbuse: -> temp_array = @get("abuse_flaggers") temp_array.push(window.user.get('id')) @@ -123,7 +125,7 @@ if Backbone? unvote: -> @incrementVote(-1) - + class @Thread extends @Content urlMappers: 'retrieve' : -> DiscussionUtil.urlFor('retrieve_single_thread', @.get('commentable_id'), @id) diff --git a/common/static/coffee/src/discussion/views/discussion_content_view.coffee b/common/static/coffee/src/discussion/views/discussion_content_view.coffee index 6e2375e718..35d3280597 100644 --- a/common/static/coffee/src/discussion/views/discussion_content_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_content_view.coffee @@ -33,6 +33,12 @@ if Backbone? [".action-close", ".action-pin"], (selector) => @$(selector).closest(".actions-item").addClass("is-hidden") ) + can_report: + enable: -> @$(".action-report").closest(".actions-item").removeClass("is-hidden") + disable: -> @$(".action-report").closest(".actions-item").addClass("is-hidden") + can_vote: + enable: -> @$(".action-vote").closest(".actions-item").removeClass("is-hidden") + disable: -> @$(".action-vote").closest(".actions-item").addClass("is-hidden") renderPartialAttrs: -> for attr, value of @model.changedAttributes() diff --git a/common/test/acceptance/pages/lms/discussion.py b/common/test/acceptance/pages/lms/discussion.py index ffb4d82b01..f02322b05b 100644 --- a/common/test/acceptance/pages/lms/discussion.py +++ b/common/test/acceptance/pages/lms/discussion.py @@ -181,6 +181,10 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin): "Response is voted" ).fulfill() + def cannot_vote_response(self, response_id): + """Assert that the voting button is not visible on this response""" + return not self.is_element_visible(".response_{} .discussion-response .action-vote".format(response_id)) + def is_response_reported(self, response_id): return self.is_element_visible(".response_{} .discussion-response .post-label-reported".format(response_id)) @@ -193,6 +197,10 @@ class DiscussionThreadPage(PageObject, DiscussionPageMixin): "Response is reported" ).fulfill() + def cannot_report_response(self, response_id): + """Assert that the reporting button is not visible on this response""" + return not self.is_element_visible(".response_{} .discussion-response .action-report".format(response_id)) + def is_response_endorsed(self, response_id): return "endorsed" in self._get_element_text(".response_{} .discussion-response .posted-details".format(response_id)) diff --git a/common/test/acceptance/tests/discussion/test_discussion.py b/common/test/acceptance/tests/discussion/test_discussion.py index dfe30c79a1..055285a39f 100644 --- a/common/test/acceptance/tests/discussion/test_discussion.py +++ b/common/test/acceptance/tests/discussion/test_discussion.py @@ -693,11 +693,11 @@ class DiscussionResponseEditTest(BaseDiscussionTestCase): And I try to edit the response created by other users Then the response should be edited and rendered successfully And I try to vote the response created by moderator - Then the response should be voted successfully + Then the response should not be able to be voted And I try to vote the response created by other users Then the response should be voted successfully And I try to report the response created by moderator - Then the response should be reported successfully + Then the response should not be able to be reported And I try to report the response created by other users Then the response should be reported successfully And I try to endorse the response created by moderator @@ -711,9 +711,9 @@ class DiscussionResponseEditTest(BaseDiscussionTestCase): page.visit() self.edit_response(page, "response_self_author") self.edit_response(page, "response_other_author") - page.vote_response('response_self_author') + page.cannot_vote_response('response_self_author') page.vote_response('response_other_author') - page.report_response('response_self_author') + page.cannot_report_response('response_self_author') page.report_response('response_other_author') page.endorse_response('response_self_author') page.endorse_response('response_other_author') diff --git a/lms/djangoapps/django_comment_client/tests/test_utils.py b/lms/djangoapps/django_comment_client/tests/test_utils.py index c8d13e8751..3726195886 100644 --- a/lms/djangoapps/django_comment_client/tests/test_utils.py +++ b/lms/djangoapps/django_comment_client/tests/test_utils.py @@ -1360,3 +1360,61 @@ class IsCommentableCohortedTestCase(ModuleStoreTestCase): # Verify that team discussions are not cohorted, but other discussions are self.assertFalse(utils.is_commentable_cohorted(course.id, team.discussion_topic_id)) self.assertTrue(utils.is_commentable_cohorted(course.id, "random")) + + +class PermissionsTestCase(ModuleStoreTestCase): + """Test utils functionality related to forums "abilities" (permissions)""" + + def test_get_ability(self): + content = {} + content['user_id'] = '1' + content['type'] = 'thread' + + user = mock.Mock() + user.id = 1 + + with mock.patch('django_comment_client.utils.check_permissions_by_view') as check_perm: + check_perm.return_value = True + self.assertEqual(utils.get_ability(None, content, user), { + 'editable': True, + 'can_reply': True, + 'can_delete': True, + 'can_openclose': True, + 'can_vote': False, + 'can_report': False + }) + + content['user_id'] = '2' + self.assertEqual(utils.get_ability(None, content, user), { + 'editable': True, + 'can_reply': True, + 'can_delete': True, + 'can_openclose': True, + 'can_vote': True, + 'can_report': True + }) + + def test_is_content_authored_by(self): + content = {} + user = mock.Mock() + user.id = 1 + + # strict equality checking + content['user_id'] = 1 + self.assertTrue(utils.is_content_authored_by(content, user)) + + # cast from string to int + content['user_id'] = '1' + self.assertTrue(utils.is_content_authored_by(content, user)) + + # strict equality checking, fails + content['user_id'] = 2 + self.assertFalse(utils.is_content_authored_by(content, user)) + + # cast from string to int, fails + content['user_id'] = 'string' + self.assertFalse(utils.is_content_authored_by(content, user)) + + # content has no known author + del content['user_id'] + self.assertFalse(utils.is_content_authored_by(content, user)) diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 801842a9f7..54a1fc2f78 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -510,7 +510,18 @@ def get_ability(course_id, content, user): 'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"), 'can_delete': check_permissions_by_view(user, course_id, content, "delete_thread" if content['type'] == 'thread' else "delete_comment"), 'can_openclose': check_permissions_by_view(user, course_id, content, "openclose_thread") if content['type'] == 'thread' else False, - 'can_vote': check_permissions_by_view(user, course_id, content, "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment"), + 'can_vote': not is_content_authored_by(content, user) and check_permissions_by_view( + user, + course_id, + content, + "vote_for_thread" if content['type'] == 'thread' else "vote_for_comment" + ), + 'can_report': not is_content_authored_by(content, user) and check_permissions_by_view( + user, + course_id, + content, + "flag_abuse_for_thread" if content['type'] == 'thread' else "flag_abuse_for_comment" + ) } # TODO: RENAME @@ -798,3 +809,13 @@ def is_discussion_enabled(course_id): if get_current_ccx(course_id): return False return settings.FEATURES.get('ENABLE_DISCUSSION_SERVICE') + + +def is_content_authored_by(content, user): + """ + Return True if the author is this content is the passed user, else False + """ + try: + return int(content.get('user_id')) == user.id + except (ValueError, TypeError): + return False