Allow authors of forum questions to mark answers
Co-authored-by: jsa <jsa@edx.org>
This commit is contained in:
@@ -6,7 +6,7 @@ from django.test.utils import override_settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.core.urlresolvers import reverse
|
||||
from mock import patch, ANY
|
||||
from mock import patch, ANY, Mock
|
||||
from nose.tools import assert_true, assert_equal # pylint: disable=E0611
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
|
||||
@@ -26,9 +26,11 @@ CS_PREFIX = "http://localhost:4567/api/v1"
|
||||
|
||||
|
||||
class MockRequestSetupMixin(object):
|
||||
def _create_repsonse_mock(self, data):
|
||||
return Mock(text=json.dumps(data), json=Mock(return_value=data))\
|
||||
|
||||
def _set_mock_request_data(self, mock_request, data):
|
||||
mock_request.return_value.text = json.dumps(data)
|
||||
mock_request.return_value.json.return_value = data
|
||||
mock_request.return_value = self._create_repsonse_mock(data)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
@@ -620,6 +622,53 @@ class ViewPermissionsTestCase(UrlResetMixin, ModuleStoreTestCase, MockRequestSet
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def _set_mock_request_thread_and_comment(self, mock_request, thread_data, comment_data):
|
||||
def handle_request(*args, **kwargs):
|
||||
url = args[1]
|
||||
if "/threads/" in url:
|
||||
return self._create_repsonse_mock(thread_data)
|
||||
elif "/comments/" in url:
|
||||
return self._create_repsonse_mock(comment_data)
|
||||
else:
|
||||
raise ArgumentError("Bad url to mock request")
|
||||
mock_request.side_effect = handle_request
|
||||
|
||||
def test_endorse_response_as_staff(self, mock_request):
|
||||
self._set_mock_request_thread_and_comment(
|
||||
mock_request,
|
||||
{"type": "thread", "thread_type": "question", "user_id": str(self.student.id)},
|
||||
{"type": "comment", "thread_id": "dummy"}
|
||||
)
|
||||
self.client.login(username=self.moderator.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("endorse_comment", kwargs={"course_id": self.course.id.to_deprecated_string(), "comment_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_endorse_response_as_student(self, mock_request):
|
||||
self._set_mock_request_thread_and_comment(
|
||||
mock_request,
|
||||
{"type": "thread", "thread_type": "question", "user_id": str(self.moderator.id)},
|
||||
{"type": "comment", "thread_id": "dummy"}
|
||||
)
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("endorse_comment", kwargs={"course_id": self.course.id.to_deprecated_string(), "comment_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_endorse_response_as_student_question_author(self, mock_request):
|
||||
self._set_mock_request_thread_and_comment(
|
||||
mock_request,
|
||||
{"type": "thread", "thread_type": "question", "user_id": str(self.student.id)},
|
||||
{"type": "comment", "thread_id": "dummy"}
|
||||
)
|
||||
self.client.login(username=self.student.username, password=self.password)
|
||||
response = self.client.post(
|
||||
reverse("endorse_comment", kwargs={"course_id": self.course.id.to_deprecated_string(), "comment_id": "dummy"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MIXED_MODULESTORE)
|
||||
class CreateThreadUnicodeTestCase(ModuleStoreTestCase, UnicodeTestMixin, MockRequestSetupMixin):
|
||||
|
||||
@@ -5,6 +5,7 @@ Module for checking permissions with the comment_client backend
|
||||
import logging
|
||||
from types import NoneType
|
||||
from django.core import cache
|
||||
from lms.lib.comment_client import Thread
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
CACHE = cache.get_cache('default')
|
||||
@@ -34,7 +35,7 @@ def has_permission(user, permission, course_id=None):
|
||||
return False
|
||||
|
||||
|
||||
CONDITIONS = ['is_open', 'is_author']
|
||||
CONDITIONS = ['is_open', 'is_author', 'is_question_author']
|
||||
|
||||
|
||||
def _check_condition(user, condition, content):
|
||||
@@ -50,9 +51,22 @@ def _check_condition(user, condition, content):
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def check_question_author(user, content):
|
||||
if not content:
|
||||
return False
|
||||
try:
|
||||
if content["type"] == "thread":
|
||||
return content["thread_type"] == "question" and content["user_id"] == str(user.id)
|
||||
else:
|
||||
# N.B. This will trigger a comments service query
|
||||
return check_question_author(user, Thread(id=content["thread_id"]).to_dict())
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
handlers = {
|
||||
'is_open': check_open,
|
||||
'is_author': check_author,
|
||||
'is_question_author': check_question_author,
|
||||
}
|
||||
|
||||
return handlers[condition](user, content)
|
||||
@@ -85,7 +99,7 @@ VIEW_PERMISSIONS = {
|
||||
'create_comment': [["create_comment", "is_open"]],
|
||||
'delete_thread': ['delete_thread', ['update_thread', 'is_author']],
|
||||
'update_comment': ['edit_content', ['update_comment', 'is_open', 'is_author']],
|
||||
'endorse_comment': ['endorse_comment'],
|
||||
'endorse_comment': ['endorse_comment', 'is_question_author'],
|
||||
'openclose_thread': ['openclose_thread'],
|
||||
'create_sub_comment': [['create_sub_comment', 'is_open']],
|
||||
'delete_comment': ['delete_comment', ['update_comment', 'is_open', 'is_author']],
|
||||
|
||||
@@ -258,7 +258,6 @@ def get_ability(course_id, content, user):
|
||||
return {
|
||||
'editable': check_permissions_by_view(user, course_id, content, "update_thread" if content['type'] == 'thread' else "update_comment"),
|
||||
'can_reply': check_permissions_by_view(user, course_id, content, "create_comment" if content['type'] == 'thread' else "create_sub_comment"),
|
||||
'can_endorse': check_permissions_by_view(user, course_id, content, "endorse_comment") if content['type'] == 'comment' else False,
|
||||
'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"),
|
||||
|
||||
@@ -647,6 +647,11 @@ body.discussion {
|
||||
border: 1px solid #a0a0a0;
|
||||
@include linear-gradient(top, $white 35%, $gray-l4);
|
||||
box-shadow: 0 1px 1px $shadow-l1;
|
||||
cursor: default;
|
||||
|
||||
&.is-clickable {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
display: block;
|
||||
@@ -654,6 +659,7 @@ body.discussion {
|
||||
height: 12px;
|
||||
margin: 8px auto;
|
||||
background: url(../images/endorse-icon.png) no-repeat;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.mark-answer .check-icon {
|
||||
|
||||
@@ -162,10 +162,9 @@
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="endorse-btn action-endorse ${"<%= thread.get('thread_type') == 'question' ? 'mark-answer' : '' %>"}"
|
||||
style="cursor: default; display: none;"
|
||||
data-tooltip="${tooltip_expr}"
|
||||
>
|
||||
<span class="check-icon" style="pointer-events: none; "></span>
|
||||
<span class="check-icon"></span>
|
||||
</a>
|
||||
${"<% if (obj.username) { %>"}
|
||||
<a href="${'<%- user_url %>'}" class="posted-by">${'<%- username %>'}</a>
|
||||
|
||||
Reference in New Issue
Block a user