feat!: remove cs_comments_service support for forum's flag APIs
This will force the use of the new v2 forum's APIs for flaging/unflaging.
This commit is contained in:
committed by
David Ormsbee
parent
610906218a
commit
6f522f3992
@@ -202,14 +202,6 @@ class ThreadActionGroupIdTestCase(
|
||||
response = self.call_view("undo_vote_for_thread", mock_is_forum_v2_enabled, mock_request)
|
||||
self._assert_json_response_contains_group_info(response)
|
||||
|
||||
def test_flag(self, mock_is_forum_v2_enabled, mock_request):
|
||||
with mock.patch('openedx.core.djangoapps.django_comment_common.signals.thread_flagged.send') as signal_mock:
|
||||
response = self.call_view("flag_abuse_for_thread", mock_is_forum_v2_enabled, mock_request)
|
||||
self._assert_json_response_contains_group_info(response)
|
||||
self.assertEqual(signal_mock.call_count, 1)
|
||||
response = self.call_view("un_flag_abuse_for_thread", mock_is_forum_v2_enabled, mock_request)
|
||||
self._assert_json_response_contains_group_info(response)
|
||||
|
||||
def test_openclose(self, mock_is_forum_v2_enabled, mock_request):
|
||||
response = self.call_view(
|
||||
"openclose_thread",
|
||||
@@ -799,309 +791,6 @@ class ViewsTestCase(
|
||||
data={"body": updated_body, "course_id": str(self.course_id)}
|
||||
)
|
||||
|
||||
def test_flag_thread_open(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.flag_thread(mock_is_forum_v2_enabled, mock_request, False)
|
||||
|
||||
def test_flag_thread_close(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.flag_thread(mock_is_forum_v2_enabled, mock_request, True)
|
||||
|
||||
def flag_thread(self, mock_is_forum_v2_enabled, mock_request, is_closed):
|
||||
mock_is_forum_v2_enabled.return_value = False
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"title": "Hello",
|
||||
"body": "this is a post",
|
||||
"course_id": "MITx/999/Robot_Super_Course",
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"commentable_id": "i4x-MITx-999-course-Robot_Super_Course",
|
||||
"created_at": "2013-05-10T18:53:43Z",
|
||||
"updated_at": "2013-05-10T18:53:43Z",
|
||||
"at_position_list": [],
|
||||
"closed": is_closed,
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"user_id": "1", "username": "robot",
|
||||
"votes": {
|
||||
"count": 0,
|
||||
"up_count": 0,
|
||||
"down_count": 0,
|
||||
"point": 0
|
||||
},
|
||||
"abuse_flaggers": [1],
|
||||
"type": "thread",
|
||||
"group_id": None,
|
||||
"pinned": False,
|
||||
"endorsed": False,
|
||||
"unread_comments_count": 0,
|
||||
"read": False,
|
||||
"comments_count": 0,
|
||||
})
|
||||
url = reverse('flag_abuse_for_thread', kwargs={
|
||||
'thread_id': '518d4237b023791dca00000d',
|
||||
'course_id': str(self.course_id)
|
||||
})
|
||||
response = self.client.post(url)
|
||||
assert mock_request.called
|
||||
|
||||
call_list = [
|
||||
(
|
||||
('get', f'{CS_PREFIX}/threads/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'mark_as_read': True, 'request_id': ANY, 'with_responses': False, 'reverse_order': False,
|
||||
'merge_question_type_responses': False},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('put', f'{CS_PREFIX}/threads/518d4237b023791dca00000d/abuse_flag'),
|
||||
{
|
||||
'data': {'user_id': '1'},
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('get', f'{CS_PREFIX}/threads/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'mark_as_read': True, 'request_id': ANY, 'with_responses': False, 'reverse_order': False,
|
||||
'merge_question_type_responses': False},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
assert mock_request.call_args_list == call_list
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_un_flag_thread_open(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.un_flag_thread(mock_is_forum_v2_enabled, mock_request, False)
|
||||
|
||||
def test_un_flag_thread_close(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.un_flag_thread(mock_is_forum_v2_enabled, mock_request, True)
|
||||
|
||||
def un_flag_thread(self, mock_is_forum_v2_enabled, mock_request, is_closed):
|
||||
mock_is_forum_v2_enabled.return_value = False
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"title": "Hello",
|
||||
"body": "this is a post",
|
||||
"course_id": "MITx/999/Robot_Super_Course",
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"commentable_id": "i4x-MITx-999-course-Robot_Super_Course",
|
||||
"created_at": "2013-05-10T18:53:43Z",
|
||||
"updated_at": "2013-05-10T18:53:43Z",
|
||||
"at_position_list": [],
|
||||
"closed": is_closed,
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"votes": {
|
||||
"count": 0,
|
||||
"up_count": 0,
|
||||
"down_count": 0,
|
||||
"point": 0
|
||||
},
|
||||
"abuse_flaggers": [],
|
||||
"type": "thread",
|
||||
"group_id": None,
|
||||
"pinned": False,
|
||||
"endorsed": False,
|
||||
"unread_comments_count": 0,
|
||||
"read": False,
|
||||
"comments_count": 0
|
||||
})
|
||||
url = reverse('un_flag_abuse_for_thread', kwargs={
|
||||
'thread_id': '518d4237b023791dca00000d',
|
||||
'course_id': str(self.course_id)
|
||||
})
|
||||
response = self.client.post(url)
|
||||
assert mock_request.called
|
||||
|
||||
call_list = [
|
||||
(
|
||||
('get', f'{CS_PREFIX}/threads/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'mark_as_read': True, 'request_id': ANY, 'with_responses': False, 'reverse_order': False,
|
||||
'merge_question_type_responses': False},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('put', f'{CS_PREFIX}/threads/518d4237b023791dca00000d/abuse_unflag'),
|
||||
{
|
||||
'data': {'user_id': '1'},
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('get', f'{CS_PREFIX}/threads/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'mark_as_read': True, 'request_id': ANY, 'with_responses': False, 'reverse_order': False,
|
||||
'merge_question_type_responses': False},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
assert mock_request.call_args_list == call_list
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_flag_comment_open(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.flag_comment(mock_is_forum_v2_enabled, mock_request, False)
|
||||
|
||||
def test_flag_comment_close(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.flag_comment(mock_is_forum_v2_enabled, mock_request, True)
|
||||
|
||||
def flag_comment(self, mock_is_forum_v2_enabled, mock_request, is_closed):
|
||||
mock_is_forum_v2_enabled.return_value = False
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"body": "this is a comment",
|
||||
"course_id": "MITx/999/Robot_Super_Course",
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"commentable_id": "i4x-MITx-999-course-Robot_Super_Course",
|
||||
"created_at": "2013-05-10T18:53:43Z",
|
||||
"updated_at": "2013-05-10T18:53:43Z",
|
||||
"at_position_list": [],
|
||||
"closed": is_closed,
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"votes": {
|
||||
"count": 0,
|
||||
"up_count": 0,
|
||||
"down_count": 0,
|
||||
"point": 0
|
||||
},
|
||||
"abuse_flaggers": [1],
|
||||
"type": "comment",
|
||||
"endorsed": False
|
||||
})
|
||||
url = reverse('flag_abuse_for_comment', kwargs={
|
||||
'comment_id': '518d4237b023791dca00000d',
|
||||
'course_id': str(self.course_id)
|
||||
})
|
||||
response = self.client.post(url)
|
||||
assert mock_request.called
|
||||
|
||||
call_list = [
|
||||
(
|
||||
('get', f'{CS_PREFIX}/comments/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('put', f'{CS_PREFIX}/comments/518d4237b023791dca00000d/abuse_flag'),
|
||||
{
|
||||
'data': {'user_id': '1'},
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('get', f'{CS_PREFIX}/comments/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
assert mock_request.call_args_list == call_list
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_un_flag_comment_open(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.un_flag_comment(mock_is_forum_v2_enabled, mock_request, False)
|
||||
|
||||
def test_un_flag_comment_close(self, mock_is_forum_v2_enabled, mock_request):
|
||||
self.un_flag_comment(mock_is_forum_v2_enabled, mock_request, True)
|
||||
|
||||
def un_flag_comment(self, mock_is_forum_v2_enabled, mock_request, is_closed):
|
||||
mock_is_forum_v2_enabled.return_value = False
|
||||
self._set_mock_request_data(mock_request, {
|
||||
"body": "this is a comment",
|
||||
"course_id": "MITx/999/Robot_Super_Course",
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"commentable_id": "i4x-MITx-999-course-Robot_Super_Course",
|
||||
"created_at": "2013-05-10T18:53:43Z",
|
||||
"updated_at": "2013-05-10T18:53:43Z",
|
||||
"at_position_list": [],
|
||||
"closed": is_closed,
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"votes": {
|
||||
"count": 0,
|
||||
"up_count": 0,
|
||||
"down_count": 0,
|
||||
"point": 0
|
||||
},
|
||||
"abuse_flaggers": [],
|
||||
"type": "comment",
|
||||
"endorsed": False
|
||||
})
|
||||
url = reverse('un_flag_abuse_for_comment', kwargs={
|
||||
'comment_id': '518d4237b023791dca00000d',
|
||||
'course_id': str(self.course_id)
|
||||
})
|
||||
response = self.client.post(url)
|
||||
assert mock_request.called
|
||||
|
||||
call_list = [
|
||||
(
|
||||
('get', f'{CS_PREFIX}/comments/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('put', f'{CS_PREFIX}/comments/518d4237b023791dca00000d/abuse_unflag'),
|
||||
{
|
||||
'data': {'user_id': '1'},
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
),
|
||||
(
|
||||
('get', f'{CS_PREFIX}/comments/518d4237b023791dca00000d'),
|
||||
{
|
||||
'data': None,
|
||||
'params': {'request_id': ANY},
|
||||
'headers': ANY,
|
||||
'timeout': 5
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
assert mock_request.call_args_list == call_list
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@ddt.data(
|
||||
('upvote_thread', 'thread_id', 'thread_voted'),
|
||||
('upvote_comment', 'comment_id', 'comment_voted'),
|
||||
@@ -1427,56 +1116,6 @@ class UpdateCommentUnicodeTestCase(
|
||||
assert mock_request.call_args[1]['data']['body'] == text
|
||||
|
||||
|
||||
@patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True)
|
||||
@patch('openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', autospec=True)
|
||||
class CommentActionTestCase(
|
||||
MockRequestSetupMixin,
|
||||
CohortedTestCase,
|
||||
GroupIdAssertionMixin
|
||||
):
|
||||
def call_view(
|
||||
self,
|
||||
view_name,
|
||||
mock_is_forum_v2_enabled,
|
||||
mock_request,
|
||||
user=None,
|
||||
post_params=None,
|
||||
view_args=None
|
||||
):
|
||||
mock_is_forum_v2_enabled.return_value = False
|
||||
patcher = mock.patch(
|
||||
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
|
||||
)
|
||||
self.mock_get_course_id_by_comment = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self._set_mock_request_data(
|
||||
mock_request,
|
||||
{
|
||||
"user_id": str(self.student.id),
|
||||
"group_id": self.student_cohort.id,
|
||||
"closed": False,
|
||||
"type": "thread",
|
||||
"commentable_id": "non_team_dummy_id",
|
||||
"body": "test body",
|
||||
}
|
||||
)
|
||||
request = RequestFactory().post("dummy_url", post_params or {})
|
||||
request.user = user or self.student
|
||||
request.view_name = view_name
|
||||
|
||||
return getattr(views, view_name)(
|
||||
request,
|
||||
course_id=str(self.course.id),
|
||||
comment_id="dummy",
|
||||
**(view_args or {})
|
||||
)
|
||||
|
||||
def test_flag(self, mock_is_forum_v2_enabled, mock_request):
|
||||
with mock.patch('openedx.core.djangoapps.django_comment_common.signals.comment_flagged.send') as signal_mock:
|
||||
self.call_view("flag_abuse_for_comment", mock_is_forum_v2_enabled, mock_request)
|
||||
self.assertEqual(signal_mock.call_count, 1)
|
||||
|
||||
|
||||
@disable_signal(views, 'comment_created')
|
||||
class CreateSubCommentUnicodeTestCase(
|
||||
ForumsEnableMixin,
|
||||
@@ -1830,7 +1469,7 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
|
||||
"course_id": str(self.course.id)
|
||||
},
|
||||
)
|
||||
for action in ["upvote_comment", "downvote_comment", "un_flag_abuse_for_comment", "flag_abuse_for_comment"]:
|
||||
for action in ["upvote_comment", "downvote_comment"]:
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
action,
|
||||
@@ -1851,8 +1490,7 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto
|
||||
user, mock_is_forum_v2_enabled, mock_request,
|
||||
{"closed": False, "commentable_id": commentable_id, "body": "dummy body", "course_id": str(self.course.id)}
|
||||
)
|
||||
for action in ["upvote_thread", "downvote_thread", "un_flag_abuse_for_thread", "flag_abuse_for_thread",
|
||||
"follow_thread", "unfollow_thread"]:
|
||||
for action in ["upvote_thread", "downvote_thread", "follow_thread", "unfollow_thread"]:
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
action,
|
||||
|
||||
@@ -86,6 +86,11 @@ from lms.djangoapps.discussion.django_comment_client.tests.mixins import (
|
||||
MockForumApiMixin,
|
||||
)
|
||||
|
||||
from lms.djangoapps.discussion.tests.utils import (
|
||||
make_minimal_cs_thread,
|
||||
make_minimal_cs_comment,
|
||||
)
|
||||
|
||||
|
||||
@disable_signal(views, "thread_edited")
|
||||
@disable_signal(views, "thread_voted")
|
||||
@@ -111,16 +116,11 @@ class ThreadActionGroupIdTestCase(
|
||||
self, view_name, mock_function, user=None, post_params=None, view_args=None
|
||||
):
|
||||
"""Call a view with the given parameters."""
|
||||
thread_response = {
|
||||
"user_id": str(self.student.id),
|
||||
"group_id": self.student_cohort.id,
|
||||
"closed": False,
|
||||
"type": "thread",
|
||||
"commentable_id": "non_team_dummy_id",
|
||||
"body": "test body",
|
||||
}
|
||||
thread_response = make_minimal_cs_thread(
|
||||
{"user_id": str(self.student.id), "group_id": self.student_cohort.id}
|
||||
)
|
||||
|
||||
self.set_mock_return_value("get_course_id_by_thread", self.course.id)
|
||||
self.set_mock_return_value("get_course_id_by_thread", str(self.course.id))
|
||||
self.set_mock_return_value("get_thread", thread_response)
|
||||
self.set_mock_return_value(mock_function, thread_response)
|
||||
|
||||
@@ -132,9 +132,19 @@ class ThreadActionGroupIdTestCase(
|
||||
request,
|
||||
course_id=str(self.course.id),
|
||||
thread_id="dummy",
|
||||
**(view_args or {})
|
||||
**(view_args or {}),
|
||||
)
|
||||
|
||||
def test_flag(self):
|
||||
with mock.patch(
|
||||
"openedx.core.djangoapps.django_comment_common.signals.thread_flagged.send"
|
||||
) as signal_mock:
|
||||
response = self.call_view("flag_abuse_for_thread", "update_thread_flag")
|
||||
self._assert_json_response_contains_group_info(response)
|
||||
self.assertEqual(signal_mock.call_count, 1)
|
||||
response = self.call_view("un_flag_abuse_for_thread", "update_thread_flag")
|
||||
self._assert_json_response_contains_group_info(response)
|
||||
|
||||
def test_pin_thread(self):
|
||||
"""Test pinning a thread."""
|
||||
response = self.call_view("pin_thread", "pin_thread", user=self.moderator)
|
||||
@@ -146,6 +156,400 @@ class ThreadActionGroupIdTestCase(
|
||||
self._assert_json_response_contains_group_info(response)
|
||||
|
||||
|
||||
class ViewsTestCaseMixin:
|
||||
|
||||
def set_up_course(self, block_count=0):
|
||||
"""
|
||||
Creates a course, optionally with block_count discussion blocks, and
|
||||
a user with appropriate permissions.
|
||||
"""
|
||||
|
||||
# create a course
|
||||
self.course = CourseFactory.create(
|
||||
org="MITx",
|
||||
course="999",
|
||||
discussion_topics={"Some Topic": {"id": "some_topic"}},
|
||||
display_name="Robot Super Course",
|
||||
)
|
||||
self.course_id = self.course.id
|
||||
|
||||
# add some discussion blocks
|
||||
for i in range(block_count):
|
||||
BlockFactory.create(
|
||||
parent_location=self.course.location,
|
||||
category="discussion",
|
||||
discussion_id=f"id_module_{i}",
|
||||
discussion_category=f"Category {i}",
|
||||
discussion_target=f"Discussion {i}",
|
||||
)
|
||||
|
||||
# seed the forums permissions and roles
|
||||
call_command("seed_permissions_roles", str(self.course_id))
|
||||
|
||||
# Patch the comment client user save method so it does not try
|
||||
# to create a new cc user when creating a django user
|
||||
with patch("common.djangoapps.student.models.user.cc.User.save"):
|
||||
uname = "student"
|
||||
email = "student@edx.org"
|
||||
self.password = "Password1234"
|
||||
|
||||
# Create the user and make them active so we can log them in.
|
||||
self.student = UserFactory.create(
|
||||
username=uname, email=email, password=self.password
|
||||
)
|
||||
self.student.is_active = True
|
||||
self.student.save()
|
||||
|
||||
# Add a discussion moderator
|
||||
self.moderator = UserFactory.create(password=self.password)
|
||||
|
||||
# Enroll the student in the course
|
||||
CourseEnrollmentFactory(user=self.student, course_id=self.course_id)
|
||||
|
||||
# Enroll the moderator and give them the appropriate roles
|
||||
CourseEnrollmentFactory(user=self.moderator, course_id=self.course.id)
|
||||
self.moderator.roles.add(
|
||||
Role.objects.get(name="Moderator", course_id=self.course.id)
|
||||
)
|
||||
|
||||
assert self.client.login(username="student", password=self.password)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(views, "comment_flagged")
|
||||
@disable_signal(views, "thread_flagged")
|
||||
class ViewsTestCase(
|
||||
ForumsEnableMixin,
|
||||
MockForumApiMixin,
|
||||
UrlResetMixin,
|
||||
SharedModuleStoreTestCase,
|
||||
ViewsTestCaseMixin,
|
||||
MockSignalHandlerMixin,
|
||||
):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# pylint: disable=super-method-not-called
|
||||
super().setUpClassAndForumMock()
|
||||
with super().setUpClassAndTestData():
|
||||
cls.course = CourseFactory.create(
|
||||
org="MITx",
|
||||
course="999",
|
||||
discussion_topics={"Some Topic": {"id": "some_topic"}},
|
||||
display_name="Robot Super Course",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
|
||||
cls.course_id = cls.course.id
|
||||
|
||||
# seed the forums permissions and roles
|
||||
call_command("seed_permissions_roles", str(cls.course_id))
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def setUp(self):
|
||||
# Patching the ENABLE_DISCUSSION_SERVICE value affects the contents of urls.py,
|
||||
# so we need to call super.setUp() which reloads urls.py (because
|
||||
# of the UrlResetMixin)
|
||||
super().setUp()
|
||||
# Patch the comment client user save method so it does not try
|
||||
# to create a new cc user when creating a django user
|
||||
with patch("common.djangoapps.student.models.user.cc.User.save"):
|
||||
uname = "student"
|
||||
email = "student@edx.org"
|
||||
self.password = "Password1234"
|
||||
|
||||
# Create the user and make them active so we can log them in.
|
||||
self.student = UserFactory.create(
|
||||
username=uname, email=email, password=self.password
|
||||
)
|
||||
self.student.is_active = True
|
||||
self.student.save()
|
||||
|
||||
# Add a discussion moderator
|
||||
self.moderator = UserFactory.create(password=self.password)
|
||||
|
||||
# Enroll the student in the course
|
||||
CourseEnrollmentFactory(user=self.student, course_id=self.course_id)
|
||||
|
||||
# Enroll the moderator and give them the appropriate roles
|
||||
CourseEnrollmentFactory(user=self.moderator, course_id=self.course.id)
|
||||
self.moderator.roles.add(
|
||||
Role.objects.get(name="Moderator", course_id=self.course.id)
|
||||
)
|
||||
|
||||
assert self.client.login(username="student", password=self.password)
|
||||
|
||||
self.set_mock_return_value("get_course_id_by_thread", str(self.course.id))
|
||||
self.set_mock_return_value("get_course_id_by_comment", str(self.course.id))
|
||||
|
||||
@contextmanager
|
||||
def assert_discussion_signals(self, signal, user=None):
|
||||
if user is None:
|
||||
user = self.student
|
||||
with self.assert_signal_sent(
|
||||
views, signal, sender=None, user=user, exclude_args=("post",)
|
||||
):
|
||||
yield
|
||||
|
||||
def test_flag_thread_open(self):
|
||||
self.flag_thread(False)
|
||||
|
||||
def test_flag_thread_close(self):
|
||||
self.flag_thread(True)
|
||||
|
||||
def flag_thread(self, is_closed):
|
||||
thread_data = make_minimal_cs_thread(
|
||||
{
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"course_id": str(self.course_id),
|
||||
"closed": is_closed,
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"abuse_flaggers": [1],
|
||||
}
|
||||
)
|
||||
self.set_mock_return_value("get_thread", thread_data)
|
||||
self.set_mock_return_value("update_thread_flag", thread_data)
|
||||
url = reverse(
|
||||
"flag_abuse_for_thread",
|
||||
kwargs={
|
||||
"thread_id": "518d4237b023791dca00000d",
|
||||
"course_id": str(self.course_id),
|
||||
},
|
||||
)
|
||||
response = self.client.post(url)
|
||||
self.check_mock_called("update_thread_flag")
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_thread",
|
||||
0,
|
||||
thread_id="518d4237b023791dca00000d",
|
||||
params={
|
||||
"mark_as_read": True,
|
||||
"with_responses": False,
|
||||
"reverse_order": False,
|
||||
"merge_question_type_responses": False,
|
||||
},
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"update_thread_flag",
|
||||
0,
|
||||
thread_id="518d4237b023791dca00000d",
|
||||
action="flag",
|
||||
user_id=ANY,
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_thread",
|
||||
1,
|
||||
thread_id="518d4237b023791dca00000d",
|
||||
params={
|
||||
"mark_as_read": True,
|
||||
"with_responses": False,
|
||||
"reverse_order": False,
|
||||
"merge_question_type_responses": False,
|
||||
},
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_un_flag_thread_open(self):
|
||||
self.un_flag_thread(False)
|
||||
|
||||
def test_un_flag_thread_close(self):
|
||||
self.un_flag_thread(True)
|
||||
|
||||
def un_flag_thread(self, is_closed):
|
||||
thread_data = make_minimal_cs_thread(
|
||||
{
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"course_id": str(self.course_id),
|
||||
"closed": is_closed,
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"abuse_flaggers": [1],
|
||||
}
|
||||
)
|
||||
|
||||
self.set_mock_return_value("get_thread", thread_data)
|
||||
self.set_mock_return_value("update_thread_flag", thread_data)
|
||||
url = reverse(
|
||||
"un_flag_abuse_for_thread",
|
||||
kwargs={
|
||||
"thread_id": "518d4237b023791dca00000d",
|
||||
"course_id": str(self.course_id),
|
||||
},
|
||||
)
|
||||
response = self.client.post(url)
|
||||
self.check_mock_called("update_thread_flag")
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_thread",
|
||||
0,
|
||||
thread_id="518d4237b023791dca00000d",
|
||||
params={
|
||||
"mark_as_read": True,
|
||||
"with_responses": False,
|
||||
"reverse_order": False,
|
||||
"merge_question_type_responses": False,
|
||||
},
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"update_thread_flag",
|
||||
0,
|
||||
thread_id="518d4237b023791dca00000d",
|
||||
action="unflag",
|
||||
user_id=ANY,
|
||||
update_all=False,
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_thread",
|
||||
1,
|
||||
thread_id="518d4237b023791dca00000d",
|
||||
params={
|
||||
"mark_as_read": True,
|
||||
"with_responses": False,
|
||||
"reverse_order": False,
|
||||
"merge_question_type_responses": False,
|
||||
},
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_flag_comment_open(self):
|
||||
self.flag_comment(False)
|
||||
|
||||
def test_flag_comment_close(self):
|
||||
self.flag_comment(True)
|
||||
|
||||
def flag_comment(self, is_closed):
|
||||
comment_data = make_minimal_cs_comment(
|
||||
{
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"body": "this is a comment",
|
||||
"course_id": str(self.course_id),
|
||||
"closed": is_closed,
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"abuse_flaggers": [1],
|
||||
}
|
||||
)
|
||||
|
||||
self.set_mock_return_value("get_parent_comment", comment_data)
|
||||
self.set_mock_return_value("update_comment_flag", comment_data)
|
||||
url = reverse(
|
||||
"flag_abuse_for_comment",
|
||||
kwargs={
|
||||
"comment_id": "518d4237b023791dca00000d",
|
||||
"course_id": str(self.course_id),
|
||||
},
|
||||
)
|
||||
|
||||
response = self.client.post(url)
|
||||
self.check_mock_called("update_thread_flag")
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_parent_comment",
|
||||
0,
|
||||
comment_id="518d4237b023791dca00000d",
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"update_comment_flag",
|
||||
0,
|
||||
comment_id="518d4237b023791dca00000d",
|
||||
action="flag",
|
||||
user_id=ANY,
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_parent_comment",
|
||||
1,
|
||||
comment_id="518d4237b023791dca00000d",
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_un_flag_comment_open(self):
|
||||
self.un_flag_comment(False)
|
||||
|
||||
def test_un_flag_comment_close(self):
|
||||
self.un_flag_comment(True)
|
||||
|
||||
def un_flag_comment(self, is_closed):
|
||||
comment_data = make_minimal_cs_comment(
|
||||
{
|
||||
"id": "518d4237b023791dca00000d",
|
||||
"body": "this is a comment",
|
||||
"course_id": str(self.course_id),
|
||||
"closed": is_closed,
|
||||
"user_id": "1",
|
||||
"username": "robot",
|
||||
"abuse_flaggers": [],
|
||||
}
|
||||
)
|
||||
|
||||
self.set_mock_return_value("get_parent_comment", comment_data)
|
||||
self.set_mock_return_value("update_comment_flag", comment_data)
|
||||
url = reverse(
|
||||
"un_flag_abuse_for_comment",
|
||||
kwargs={
|
||||
"comment_id": "518d4237b023791dca00000d",
|
||||
"course_id": str(self.course_id),
|
||||
},
|
||||
)
|
||||
|
||||
response = self.client.post(url)
|
||||
self.check_mock_called("update_thread_flag")
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_parent_comment",
|
||||
0,
|
||||
comment_id="518d4237b023791dca00000d",
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"update_comment_flag",
|
||||
0,
|
||||
comment_id="518d4237b023791dca00000d",
|
||||
action="unflag",
|
||||
update_all=False,
|
||||
user_id=ANY,
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
self.check_mock_called_with(
|
||||
"get_parent_comment",
|
||||
1,
|
||||
comment_id="518d4237b023791dca00000d",
|
||||
course_id=str(self.course_id),
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@disable_signal(views, "comment_endorsed")
|
||||
class ViewPermissionsTestCase(
|
||||
ForumsEnableMixin,
|
||||
@@ -242,3 +646,250 @@ class ViewPermissionsTestCase(
|
||||
)
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
class CommentActionTestCase(CohortedTestCase, MockForumApiMixin):
|
||||
"""Test case for thread actions with group ID assertions."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up class and forum mock."""
|
||||
super().setUpClass()
|
||||
super().setUpClassAndForumMock()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
def call_view(
|
||||
self, view_name, mock_function, user=None, post_params=None, view_args=None
|
||||
):
|
||||
"""Call a view with the given parameters."""
|
||||
comment_response = make_minimal_cs_comment(
|
||||
{"user_id": str(self.student.id), "group_id": self.student_cohort.id}
|
||||
)
|
||||
|
||||
self.set_mock_return_value("get_course_id_by_comment", str(self.course.id))
|
||||
self.set_mock_return_value("get_parent_comment", comment_response)
|
||||
self.set_mock_return_value(mock_function, comment_response)
|
||||
|
||||
request = RequestFactory().post("dummy_url", post_params or {})
|
||||
request.user = user or self.student
|
||||
request.view_name = view_name
|
||||
|
||||
return getattr(views, view_name)(
|
||||
request,
|
||||
course_id=str(self.course.id),
|
||||
comment_id="dummy",
|
||||
**(view_args or {}),
|
||||
)
|
||||
|
||||
def test_flag(self):
|
||||
with mock.patch(
|
||||
"openedx.core.djangoapps.django_comment_common.signals.comment_flagged.send"
|
||||
) as signal_mock:
|
||||
self.call_view("flag_abuse_for_comment", "update_comment_flag")
|
||||
self.assertEqual(signal_mock.call_count, 1)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(views, "thread_voted")
|
||||
@disable_signal(views, "thread_edited")
|
||||
@disable_signal(views, "comment_created")
|
||||
@disable_signal(views, "comment_voted")
|
||||
@disable_signal(views, "comment_deleted")
|
||||
@disable_signal(views, "comment_flagged")
|
||||
@disable_signal(views, "thread_flagged")
|
||||
class TeamsPermissionsTestCase(
|
||||
UrlResetMixin, SharedModuleStoreTestCase, MockForumApiMixin
|
||||
):
|
||||
# Most of the test points use the same ddt data.
|
||||
# args: user, commentable_id, status_code
|
||||
ddt_permissions_args = [
|
||||
# Student in team can do operations on threads/comments within the team commentable.
|
||||
("student_in_team", "team_commentable_id", 200),
|
||||
# Non-team commentables can be edited by any student.
|
||||
("student_in_team", "course_commentable_id", 200),
|
||||
# Student not in team cannot do operations within the team commentable.
|
||||
("student_not_in_team", "team_commentable_id", 401),
|
||||
# Non-team commentables can be edited by any student.
|
||||
("student_not_in_team", "course_commentable_id", 200),
|
||||
# Moderators can always operator on threads within a team, regardless of team membership.
|
||||
("moderator", "team_commentable_id", 200),
|
||||
# Group moderators have regular student privileges for creating a thread and commenting
|
||||
("group_moderator", "course_commentable_id", 200),
|
||||
]
|
||||
|
||||
def change_divided_discussion_settings(self, scheme):
|
||||
"""
|
||||
Change divided discussion settings for the current course.
|
||||
If dividing by cohorts, create and assign users to a cohort.
|
||||
"""
|
||||
enable_cohorts = True if scheme is CourseDiscussionSettings.COHORT else False
|
||||
discussion_settings = CourseDiscussionSettings.get(self.course.id)
|
||||
discussion_settings.update(
|
||||
{
|
||||
"enable_cohorts": enable_cohorts,
|
||||
"divided_discussions": [],
|
||||
"always_divide_inline_discussions": True,
|
||||
"division_scheme": scheme,
|
||||
}
|
||||
)
|
||||
set_course_cohorted(self.course.id, enable_cohorts)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClassAndForumMock()
|
||||
# pylint: disable=super-method-not-called
|
||||
with super().setUpClassAndTestData():
|
||||
teams_config_data = {
|
||||
"topics": [
|
||||
{
|
||||
"id": "topic_id",
|
||||
"name": "Solar Power",
|
||||
"description": "Solar power is hot",
|
||||
}
|
||||
]
|
||||
}
|
||||
cls.course = CourseFactory.create(
|
||||
teams_configuration=TeamsConfig(teams_config_data)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
cls.password = "test password"
|
||||
seed_permissions_roles(cls.course.id)
|
||||
|
||||
# Create enrollment tracks
|
||||
CourseModeFactory.create(course_id=cls.course.id, mode_slug=CourseMode.VERIFIED)
|
||||
CourseModeFactory.create(course_id=cls.course.id, mode_slug=CourseMode.AUDIT)
|
||||
|
||||
# Create 6 users--
|
||||
# student in team (in the team, audit)
|
||||
# student not in team (not in the team, audit)
|
||||
# cohorted (in the cohort, audit)
|
||||
# verified (not in the cohort, verified)
|
||||
# moderator (in the cohort, audit, moderator permissions)
|
||||
# group moderator (in the cohort, verified, group moderator permissions)
|
||||
def create_users_and_enroll(coursemode):
|
||||
student = UserFactory.create(password=cls.password)
|
||||
CourseEnrollmentFactory(
|
||||
course_id=cls.course.id, user=student, mode=coursemode
|
||||
)
|
||||
return student
|
||||
|
||||
cls.student_in_team, cls.student_not_in_team, cls.moderator, cls.cohorted = [
|
||||
create_users_and_enroll(CourseMode.AUDIT) for _ in range(4)
|
||||
]
|
||||
cls.verified, cls.group_moderator = [
|
||||
create_users_and_enroll(CourseMode.VERIFIED) for _ in range(2)
|
||||
]
|
||||
|
||||
# Give moderator and group moderator permissions
|
||||
cls.moderator.roles.add(
|
||||
Role.objects.get(name="Moderator", course_id=cls.course.id)
|
||||
)
|
||||
assign_role(cls.course.id, cls.group_moderator, "Group Moderator")
|
||||
|
||||
# Create a team
|
||||
cls.team_commentable_id = "team_discussion_id"
|
||||
cls.team = CourseTeamFactory.create(
|
||||
name="The Only Team",
|
||||
course_id=cls.course.id,
|
||||
topic_id="topic_id",
|
||||
discussion_topic_id=cls.team_commentable_id,
|
||||
)
|
||||
CourseTeamMembershipFactory.create(team=cls.team, user=cls.student_in_team)
|
||||
|
||||
# Dummy commentable ID not linked to a team
|
||||
cls.course_commentable_id = "course_level_commentable"
|
||||
|
||||
# Create cohort and add students to it
|
||||
CohortFactory(
|
||||
course_id=cls.course.id,
|
||||
name="Test Cohort",
|
||||
users=[cls.group_moderator, cls.cohorted],
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
"django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}
|
||||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
def _setup_mock(self, user, mock_functions=[], data=None):
|
||||
user = getattr(self, user)
|
||||
mock_functions = mock_functions or []
|
||||
for mock_func in mock_functions:
|
||||
self.set_mock_return_value(mock_func, data or {})
|
||||
self.client.login(username=user.username, password=self.password)
|
||||
|
||||
@ddt.data(*ddt_permissions_args)
|
||||
@ddt.unpack
|
||||
def test_comment_actions(self, user, commentable_id, status_code):
|
||||
"""
|
||||
Verify that voting and flagging of comments is limited to members of the team or users with
|
||||
'edit_content' permission.
|
||||
"""
|
||||
commentable_id = getattr(self, commentable_id)
|
||||
self._setup_mock(
|
||||
user,
|
||||
["get_parent_comment", "update_comment_flag"],
|
||||
make_minimal_cs_comment(
|
||||
{
|
||||
"commentable_id": commentable_id,
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
),
|
||||
)
|
||||
for action in ["un_flag_abuse_for_comment", "flag_abuse_for_comment"]:
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
action,
|
||||
kwargs={
|
||||
"course_id": str(self.course.id),
|
||||
"comment_id": "dummy",
|
||||
},
|
||||
)
|
||||
)
|
||||
assert response.status_code == status_code
|
||||
|
||||
@ddt.data(*ddt_permissions_args)
|
||||
@ddt.unpack
|
||||
def test_threads_actions(self, user, commentable_id, status_code):
|
||||
"""
|
||||
Verify that voting, flagging, and following of threads is limited to members of the team or users with
|
||||
'edit_content' permission.
|
||||
"""
|
||||
commentable_id = getattr(self, commentable_id)
|
||||
self._setup_mock(
|
||||
user,
|
||||
["get_thread", "update_thread_flag"],
|
||||
make_minimal_cs_thread(
|
||||
{
|
||||
"commentable_id": commentable_id,
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
for action in ["un_flag_abuse_for_thread", "flag_abuse_for_thread"]:
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
action,
|
||||
kwargs={
|
||||
"course_id": str(self.course.id),
|
||||
"thread_id": "dummy",
|
||||
},
|
||||
)
|
||||
)
|
||||
assert response.status_code == status_code
|
||||
|
||||
@@ -8,11 +8,6 @@ from unittest import mock
|
||||
class MockForumApiMixin:
|
||||
"""Mixin to mock forum_api across different test cases with a single mock instance."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Apply a single forum_api mock at the class level."""
|
||||
cls.setUpClassAndForumMock()
|
||||
|
||||
@classmethod
|
||||
def setUpClassAndForumMock(cls):
|
||||
"""
|
||||
@@ -49,11 +44,6 @@ class MockForumApiMixin:
|
||||
for patcher in cls.forum_api_patchers:
|
||||
patcher.stop()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
cls.disposeForumMocks()
|
||||
|
||||
def set_mock_return_value(self, function_name, return_value):
|
||||
"""
|
||||
Set a return value for a specific method in forum_api mock.
|
||||
@@ -65,3 +55,50 @@ class MockForumApiMixin:
|
||||
setattr(
|
||||
self.mock_forum_api, function_name, mock.Mock(return_value=return_value)
|
||||
)
|
||||
|
||||
def set_mock_side_effect(self, function_name, side_effect_fn):
|
||||
"""
|
||||
Set a side effect for a specific method in forum_api mock.
|
||||
|
||||
Args:
|
||||
function_name (str): The method name in the mock to set a side effect for.
|
||||
side_effect_fn (Callable): A function to be called when the mock is called.
|
||||
"""
|
||||
setattr(
|
||||
self.mock_forum_api, function_name, mock.Mock(side_effect=side_effect_fn)
|
||||
)
|
||||
|
||||
def check_mock_called_with(self, function_name, index, *parms, **kwargs):
|
||||
"""
|
||||
Check if a specific method in forum_api mock was called with the given parameters.
|
||||
|
||||
Args:
|
||||
function_name (str): The method name in the mock to check.
|
||||
parms (tuple): The parameters to check the method was called with.
|
||||
"""
|
||||
call_args = getattr(self.mock_forum_api, function_name).call_args_list[index]
|
||||
assert call_args == mock.call(*parms, **kwargs)
|
||||
|
||||
def check_mock_called(self, function_name):
|
||||
"""
|
||||
Check if a specific method in the forum_api mock was called.
|
||||
|
||||
Args:
|
||||
function_name (str): The method name in the mock to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the method was called, False otherwise.
|
||||
"""
|
||||
return getattr(self.mock_forum_api, function_name).called
|
||||
|
||||
def get_mock_func_calls(self, function_name):
|
||||
"""
|
||||
Returns a list of call arguments for a specific method in the mock_forum_api.
|
||||
|
||||
Args:
|
||||
function_name (str): The name of the method in the mock_forum_api to retrieve call arguments for.
|
||||
|
||||
Returns:
|
||||
list: A list of call arguments for the specified method.
|
||||
"""
|
||||
return getattr(self.mock_forum_api, function_name).call_args_list
|
||||
|
||||
@@ -2153,18 +2153,6 @@ class CreateThreadTest(
|
||||
assert cs_request.method == 'POST'
|
||||
assert parsed_body(cs_request) == {'source_type': ['thread'], 'source_id': ['test_id']}
|
||||
|
||||
def test_abuse_flagged(self):
|
||||
self.register_post_thread_response({"id": "test_id", "username": self.user.username})
|
||||
self.register_thread_flag_response("test_id")
|
||||
data = self.minimal_data.copy()
|
||||
data["abuse_flagged"] = "True"
|
||||
result = create_thread(self.request, data)
|
||||
assert result['abuse_flagged'] is True
|
||||
cs_request = httpretty.last_request()
|
||||
assert urlparse(cs_request.path).path == '/api/v1/threads/test_id/abuse_flag' # lint-amnesty, pylint: disable=no-member
|
||||
assert cs_request.method == 'PUT'
|
||||
assert parsed_body(cs_request) == {'user_id': [str(self.user.id)]}
|
||||
|
||||
def test_course_id_missing(self):
|
||||
with pytest.raises(ValidationError) as assertion:
|
||||
create_thread(self.request, {})
|
||||
@@ -2513,18 +2501,6 @@ class CreateCommentTest(
|
||||
except ValidationError:
|
||||
assert expected_error
|
||||
|
||||
def test_abuse_flagged(self):
|
||||
self.register_post_comment_response({"id": "test_comment", "username": self.user.username}, "test_thread")
|
||||
self.register_comment_flag_response("test_comment")
|
||||
data = self.minimal_data.copy()
|
||||
data["abuse_flagged"] = "True"
|
||||
result = create_comment(self.request, data)
|
||||
assert result['abuse_flagged'] is True
|
||||
cs_request = httpretty.last_request()
|
||||
assert urlparse(cs_request.path).path == '/api/v1/comments/test_comment/abuse_flag' # lint-amnesty, pylint: disable=no-member
|
||||
assert cs_request.method == 'PUT'
|
||||
assert parsed_body(cs_request) == {'user_id': [str(self.user.id)]}
|
||||
|
||||
def test_thread_id_missing(self):
|
||||
with pytest.raises(ValidationError) as assertion:
|
||||
create_comment(self.request, {})
|
||||
@@ -2960,108 +2936,6 @@ class UpdateThreadTest(
|
||||
assert result['vote_count'] == vote_count
|
||||
self.register_get_user_response(self.user, upvoted_ids=[])
|
||||
|
||||
@ddt.data(*itertools.product([True, False], [True, False]))
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_abuse_flagged(self, old_flagged, new_flagged, mock_emit):
|
||||
"""
|
||||
Test attempts to edit the "abuse_flagged" field.
|
||||
|
||||
old_flagged indicates whether the thread should be flagged at the start
|
||||
of the test. new_flagged indicates the value for the "abuse_flagged"
|
||||
field in the update. If old_flagged and new_flagged are the same, no
|
||||
update should be made. Otherwise, a PUT should be made to the flag or
|
||||
or unflag endpoint according to the new_flagged value.
|
||||
"""
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread_flag_response("test_thread")
|
||||
self.register_thread({"abuse_flaggers": [str(self.user.id)] if old_flagged else []})
|
||||
data = {"abuse_flagged": new_flagged}
|
||||
result = update_thread(self.request, "test_thread", data)
|
||||
assert result['abuse_flagged'] == new_flagged
|
||||
last_request_path = urlparse(httpretty.last_request().path).path # lint-amnesty, pylint: disable=no-member
|
||||
flag_url = "/api/v1/threads/test_thread/abuse_flag"
|
||||
unflag_url = "/api/v1/threads/test_thread/abuse_unflag"
|
||||
if old_flagged == new_flagged:
|
||||
assert last_request_path != flag_url
|
||||
assert last_request_path != unflag_url
|
||||
else:
|
||||
assert last_request_path == (flag_url if new_flagged else unflag_url)
|
||||
assert httpretty.last_request().method == 'PUT'
|
||||
assert parsed_body(httpretty.last_request()) == {'user_id': [str(self.user.id)]}
|
||||
|
||||
expected_event_name = 'edx.forum.thread.reported' if new_flagged else 'edx.forum.thread.unreported'
|
||||
expected_event_data = {
|
||||
'body': 'Original body',
|
||||
'id': 'test_thread',
|
||||
'content_type': 'Post',
|
||||
'commentable_id': 'original_topic',
|
||||
'url': '',
|
||||
'user_course_roles': [],
|
||||
'user_forums_roles': [FORUM_ROLE_STUDENT],
|
||||
'target_username': self.user.username,
|
||||
'title_truncated': False,
|
||||
'title': 'Original Title',
|
||||
'thread_type': 'discussion',
|
||||
'group_id': None,
|
||||
'truncated': False,
|
||||
}
|
||||
if not new_flagged:
|
||||
expected_event_data['reported_status_cleared'] = False
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
@ddt.data(
|
||||
(False, True),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_thread_un_abuse_flag_for_moderator_role(self, is_author, remove_all, mock_emit):
|
||||
"""
|
||||
Test un-abuse flag for moderator role.
|
||||
|
||||
When moderator unflags a reported thread, it should
|
||||
pass the "all" flag to the api. This will indicate
|
||||
to the api to clear all abuse_flaggers, and mark the
|
||||
thread as unreported.
|
||||
"""
|
||||
_assign_role_to_user(user=self.user, course_id=self.course.id, role=FORUM_ROLE_ADMINISTRATOR)
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread_flag_response("test_thread")
|
||||
self.register_thread({"abuse_flaggers": ["11"], "user_id": str(self.user.id) if is_author else "12"})
|
||||
data = {"abuse_flagged": False}
|
||||
update_thread(self.request, "test_thread", data)
|
||||
assert httpretty.last_request().method == 'PUT'
|
||||
query_params = {'user_id': [str(self.user.id)]}
|
||||
if remove_all:
|
||||
query_params.update({'all': ['True']})
|
||||
assert parsed_body(httpretty.last_request()) == query_params
|
||||
|
||||
expected_event_name = 'edx.forum.thread.unreported'
|
||||
expected_event_data = {
|
||||
'body': 'Original body',
|
||||
'id': 'test_thread',
|
||||
'content_type': 'Post',
|
||||
'commentable_id': 'original_topic',
|
||||
'url': '',
|
||||
'user_course_roles': [],
|
||||
'user_forums_roles': [FORUM_ROLE_STUDENT, FORUM_ROLE_ADMINISTRATOR],
|
||||
'target_username': self.user.username,
|
||||
'title_truncated': False,
|
||||
'title': 'Original Title',
|
||||
'reported_status_cleared': False,
|
||||
'thread_type': 'discussion',
|
||||
'group_id': None,
|
||||
'truncated': False,
|
||||
}
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
def test_invalid_field(self):
|
||||
self.register_thread()
|
||||
with pytest.raises(ValidationError) as assertion:
|
||||
@@ -3569,100 +3443,6 @@ class UpdateCommentTest(
|
||||
assert result['vote_count'] == vote_count
|
||||
self.register_get_user_response(self.user, upvoted_ids=[])
|
||||
|
||||
@ddt.data(*itertools.product([True, False], [True, False]))
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_abuse_flagged(self, old_flagged, new_flagged, mock_emit):
|
||||
"""
|
||||
Test attempts to edit the "abuse_flagged" field.
|
||||
|
||||
old_flagged indicates whether the comment should be flagged at the start
|
||||
of the test. new_flagged indicates the value for the "abuse_flagged"
|
||||
field in the update. If old_flagged and new_flagged are the same, no
|
||||
update should be made. Otherwise, a PUT should be made to the flag or
|
||||
or unflag endpoint according to the new_flagged value.
|
||||
"""
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_comment_flag_response("test_comment")
|
||||
self.register_comment({"abuse_flaggers": [str(self.user.id)] if old_flagged else []})
|
||||
data = {"abuse_flagged": new_flagged}
|
||||
result = update_comment(self.request, "test_comment", data)
|
||||
assert result['abuse_flagged'] == new_flagged
|
||||
last_request_path = urlparse(httpretty.last_request().path).path # lint-amnesty, pylint: disable=no-member
|
||||
flag_url = "/api/v1/comments/test_comment/abuse_flag"
|
||||
unflag_url = "/api/v1/comments/test_comment/abuse_unflag"
|
||||
if old_flagged == new_flagged:
|
||||
assert last_request_path != flag_url
|
||||
assert last_request_path != unflag_url
|
||||
else:
|
||||
assert last_request_path == (flag_url if new_flagged else unflag_url)
|
||||
assert httpretty.last_request().method == 'PUT'
|
||||
assert parsed_body(httpretty.last_request()) == {'user_id': [str(self.user.id)]}
|
||||
|
||||
expected_event_name = 'edx.forum.response.reported' if new_flagged else 'edx.forum.response.unreported'
|
||||
expected_event_data = {
|
||||
'body': 'Original body',
|
||||
'id': 'test_comment',
|
||||
'content_type': 'Response',
|
||||
'commentable_id': 'dummy',
|
||||
'url': '',
|
||||
'truncated': False,
|
||||
'user_course_roles': [],
|
||||
'user_forums_roles': [FORUM_ROLE_STUDENT],
|
||||
'target_username': self.user.username,
|
||||
}
|
||||
if not new_flagged:
|
||||
expected_event_data['reported_status_cleared'] = False
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
@ddt.data(
|
||||
(False, True),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_comment_un_abuse_flag_for_moderator_role(self, is_author, remove_all, mock_emit):
|
||||
"""
|
||||
Test un-abuse flag for moderator role.
|
||||
|
||||
When moderator unflags a reported comment, it should
|
||||
pass the "all" flag to the api. This will indicate
|
||||
to the api to clear all abuse_flaggers, and mark the
|
||||
comment as unreported.
|
||||
"""
|
||||
_assign_role_to_user(user=self.user, course_id=self.course.id, role=FORUM_ROLE_ADMINISTRATOR)
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_comment_flag_response("test_comment")
|
||||
self.register_comment({"abuse_flaggers": ["11"], "user_id": str(self.user.id) if is_author else "12"})
|
||||
data = {"abuse_flagged": False}
|
||||
update_comment(self.request, "test_comment", data)
|
||||
assert httpretty.last_request().method == 'PUT'
|
||||
query_params = {'user_id': [str(self.user.id)]}
|
||||
if remove_all:
|
||||
query_params.update({'all': ['True']})
|
||||
assert parsed_body(httpretty.last_request()) == query_params
|
||||
|
||||
expected_event_name = 'edx.forum.response.unreported'
|
||||
expected_event_data = {
|
||||
'body': 'Original body',
|
||||
'id': 'test_comment',
|
||||
'content_type': 'Response',
|
||||
'commentable_id': 'dummy',
|
||||
'truncated': False,
|
||||
'url': '',
|
||||
'user_course_roles': [],
|
||||
'user_forums_roles': [FORUM_ROLE_STUDENT, FORUM_ROLE_ADMINISTRATOR],
|
||||
'target_username': self.user.username,
|
||||
'reported_status_cleared': False,
|
||||
}
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
@ddt.data(
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
FORUM_ROLE_MODERATOR,
|
||||
|
||||
758
lms/djangoapps/discussion/rest_api/tests/test_api_v2.py
Normal file
758
lms/djangoapps/discussion/rest_api/tests/test_api_v2.py
Normal file
@@ -0,0 +1,758 @@
|
||||
# pylint: skip-file
|
||||
"""
|
||||
Tests for the internal interface of the Discussion API (rest_api/api.py).
|
||||
|
||||
This module directly tests the internal API functions of the Discussion API, such as create_thread,
|
||||
create_comment, update_thread, update_comment, and related helpers, by invoking them with various data and request objects.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
import ddt
|
||||
import httpretty
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test.client import RequestFactory
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from pytz import UTC
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
SharedModuleStoreTestCase,
|
||||
)
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
|
||||
from common.djangoapps.student.tests.factories import (
|
||||
AdminFactory,
|
||||
BetaTesterFactory,
|
||||
CourseEnrollmentFactory,
|
||||
StaffFactory,
|
||||
UserFactory,
|
||||
)
|
||||
from common.djangoapps.util.testing import UrlResetMixin
|
||||
from common.test.utils import MockSignalHandlerMixin, disable_signal
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.utils import (
|
||||
ForumsEnableMixin,
|
||||
)
|
||||
from lms.djangoapps.discussion.tests.utils import (
|
||||
make_minimal_cs_comment,
|
||||
make_minimal_cs_thread,
|
||||
)
|
||||
from lms.djangoapps.discussion.rest_api import api
|
||||
from lms.djangoapps.discussion.rest_api.api import (
|
||||
create_comment,
|
||||
create_thread,
|
||||
delete_comment,
|
||||
delete_thread,
|
||||
get_comment_list,
|
||||
get_course,
|
||||
get_course_topics,
|
||||
get_course_topics_v2,
|
||||
get_thread,
|
||||
get_thread_list,
|
||||
get_user_comments,
|
||||
update_comment,
|
||||
update_thread,
|
||||
)
|
||||
from lms.djangoapps.discussion.rest_api.exceptions import (
|
||||
CommentNotFoundError,
|
||||
DiscussionBlackOutException,
|
||||
DiscussionDisabledError,
|
||||
ThreadNotFoundError,
|
||||
)
|
||||
from lms.djangoapps.discussion.rest_api.serializers import TopicOrdering
|
||||
from lms.djangoapps.discussion.rest_api.tests.utils import (
|
||||
CommentsServiceMockMixin,
|
||||
ForumMockUtilsMixin,
|
||||
make_paginated_api_response,
|
||||
parsed_body,
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
|
||||
from openedx.core.djangoapps.discussions.models import (
|
||||
DiscussionsConfiguration,
|
||||
DiscussionTopicLink,
|
||||
Provider,
|
||||
PostingRestriction,
|
||||
)
|
||||
from openedx.core.djangoapps.discussions.tasks import (
|
||||
update_discussions_settings_from_course_task,
|
||||
)
|
||||
from openedx.core.djangoapps.django_comment_common.models import (
|
||||
FORUM_ROLE_ADMINISTRATOR,
|
||||
FORUM_ROLE_COMMUNITY_TA,
|
||||
FORUM_ROLE_MODERATOR,
|
||||
FORUM_ROLE_STUDENT,
|
||||
Role,
|
||||
)
|
||||
from openedx.core.lib.exceptions import CourseNotFoundError, PageNotFoundError
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def _remove_discussion_tab(course, user_id):
|
||||
"""
|
||||
Remove the discussion tab for the course.
|
||||
|
||||
user_id is passed to the modulestore as the editor of the xblock.
|
||||
"""
|
||||
course.tabs = [tab for tab in course.tabs if not tab.type == "discussion"]
|
||||
modulestore().update_item(course, user_id)
|
||||
|
||||
|
||||
def _discussion_disabled_course_for(user):
|
||||
"""
|
||||
Create and return a course with discussions disabled.
|
||||
|
||||
The user passed in will be enrolled in the course.
|
||||
"""
|
||||
course_with_disabled_forums = CourseFactory.create()
|
||||
CourseEnrollmentFactory.create(user=user, course_id=course_with_disabled_forums.id)
|
||||
_remove_discussion_tab(course_with_disabled_forums, user.id)
|
||||
|
||||
return course_with_disabled_forums
|
||||
|
||||
|
||||
def _assign_role_to_user(user, course_id, role):
|
||||
"""
|
||||
Assign a discussion role to a user for a given course.
|
||||
|
||||
Arguments:
|
||||
user: User to assign role to
|
||||
course_id: Course id of the course user will be assigned role in
|
||||
role: Role assigned to user for course
|
||||
"""
|
||||
role = Role.objects.create(name=role, course_id=course_id)
|
||||
role.users.set([user])
|
||||
|
||||
|
||||
def _create_course_and_cohort_with_user_role(course_is_cohorted, user, role_name):
|
||||
"""
|
||||
Creates a course with the value of `course_is_cohorted`, plus `always_cohort_inline_discussions`
|
||||
set to True (which is no longer the default value). Then 1) enrolls the user in that course,
|
||||
2) creates a cohort that the user is placed in, and 3) adds the user to the given role.
|
||||
|
||||
Returns: a tuple of the created course and the created cohort
|
||||
"""
|
||||
cohort_course = CourseFactory.create(
|
||||
cohort_config={
|
||||
"cohorted": course_is_cohorted,
|
||||
"always_cohort_inline_discussions": True,
|
||||
}
|
||||
)
|
||||
CourseEnrollmentFactory.create(user=user, course_id=cohort_course.id)
|
||||
cohort = CohortFactory.create(course_id=cohort_course.id, users=[user])
|
||||
_assign_role_to_user(user=user, course_id=cohort_course.id, role=role_name)
|
||||
|
||||
return [cohort_course, cohort]
|
||||
|
||||
|
||||
def _set_course_discussion_blackout(course, user_id):
|
||||
"""
|
||||
Set the blackout period for course discussions.
|
||||
|
||||
Arguments:
|
||||
course: Course for which blackout period is set
|
||||
user_id: User id of user enrolled in the course
|
||||
"""
|
||||
course.discussion_blackouts = [
|
||||
datetime.now(UTC) - timedelta(days=3),
|
||||
datetime.now(UTC) + timedelta(days=3),
|
||||
]
|
||||
configuration = DiscussionsConfiguration.get(course.id)
|
||||
configuration.posting_restrictions = PostingRestriction.SCHEDULED
|
||||
configuration.save()
|
||||
modulestore().update_item(course, user_id)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(api, "thread_created")
|
||||
@disable_signal(api, "thread_voted")
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class CreateThreadTest(
|
||||
ForumsEnableMixin,
|
||||
UrlResetMixin,
|
||||
SharedModuleStoreTestCase,
|
||||
MockSignalHandlerMixin,
|
||||
ForumMockUtilsMixin,
|
||||
):
|
||||
"""Tests for create_thread"""
|
||||
|
||||
LONG_TITLE = (
|
||||
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. "
|
||||
"Aenean commodo ligula eget dolor. Aenean massa. Cum sociis "
|
||||
"natoque penatibus et magnis dis parturient montes, nascetur "
|
||||
"ridiculus mus. Donec quam felis, ultricies nec, "
|
||||
"pellentesque eu, pretium quis, sem. Nulla consequat massa "
|
||||
"quis enim. Donec pede justo, fringilla vel, aliquet nec, "
|
||||
"vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet "
|
||||
"a, venenatis vitae, justo. Nullam dictum felis eu pede "
|
||||
"mollis pretium. Integer tincidunt. Cras dapibus. Vivamus "
|
||||
"elementum semper nisi. Aenean vulputate eleifend tellus. "
|
||||
"Aenean leo ligula, porttitor eu, consequat vitae, eleifend "
|
||||
"ac, enim. Aliquam lorem ante, dapibus in, viverra quis, "
|
||||
"feugiat a, tellus. Phasellus viverra nulla ut metus varius "
|
||||
"laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies "
|
||||
"nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam "
|
||||
"eget dui. Etiam rhoncus. Maecenas tempus, tellus eget "
|
||||
"condimentum rhoncus, sem quam semper libero, sit amet "
|
||||
"adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, "
|
||||
"luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et "
|
||||
"ante tincidunt tempus. Donec vitae sapien ut libero "
|
||||
"venenatis faucibus. Nullam quis ante. Etiam sit amet orci "
|
||||
"eget eros faucibus tincidunt. Duis leo. Sed fringilla "
|
||||
"mauris sit amet nibh. Donec sodales sagittis magna. Sed "
|
||||
"consequat, leo eget bibendum sodales, augue velit cursus "
|
||||
"nunc, quis gravida magna mi a libero. Fusce vulputate "
|
||||
"eleifend sapien. Vestibulum purus quam, scelerisque ut, "
|
||||
"mollis sed, nonummy id, metus. Nullam accumsan lorem in "
|
||||
"dui. Cras ultricies mi eu turpis hendrerit fringilla. "
|
||||
"Vestibulum ante ipsum primis in faucibus orci luctus et "
|
||||
"ultrices posuere cubilia Curae; In ac dui quis mi "
|
||||
"consectetuer lacinia. Nam pretium turpis et arcu. Duis arcu "
|
||||
"tortor, suscipit eget, imperdiet nec, imperdiet iaculis, "
|
||||
"ipsum. Sed aliquam ultrices mauris. Integer ante arcu, "
|
||||
"accumsan a, consectetuer eget, posuere ut, mauris. Praesent "
|
||||
"adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc "
|
||||
"nonummy metus."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
super().setUpClassAndForumMock()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
@mock.patch.dict(
|
||||
"django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}
|
||||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.course = CourseFactory.create()
|
||||
httpretty.reset()
|
||||
httpretty.enable()
|
||||
self.addCleanup(httpretty.reset)
|
||||
self.addCleanup(httpretty.disable)
|
||||
self.user = UserFactory.create()
|
||||
self.register_get_user_response(self.user)
|
||||
self.request = RequestFactory().get("/test_path")
|
||||
self.request.user = self.user
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
self.minimal_data = {
|
||||
"course_id": str(self.course.id),
|
||||
"topic_id": "test_topic",
|
||||
"type": "discussion",
|
||||
"title": "Test Title",
|
||||
"raw_body": "Test body",
|
||||
}
|
||||
|
||||
def test_abuse_flagged(self):
|
||||
self.register_post_thread_response(
|
||||
{"id": "test_id", "username": self.user.username}
|
||||
)
|
||||
self.register_thread_flag_response("test_id")
|
||||
data = self.minimal_data.copy()
|
||||
data["abuse_flagged"] = "True"
|
||||
result = create_thread(self.request, data)
|
||||
assert result["abuse_flagged"] is True
|
||||
|
||||
self.check_mock_called("update_thread_flag")
|
||||
params = {
|
||||
"thread_id": "test_id",
|
||||
"action": "flag",
|
||||
"user_id": "1",
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
self.check_mock_called_with("update_thread_flag", -1, **params)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(api, "comment_created")
|
||||
@disable_signal(api, "comment_voted")
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
@mock.patch(
|
||||
"lms.djangoapps.discussion.signals.handlers.send_response_notifications",
|
||||
new=mock.Mock(),
|
||||
)
|
||||
class CreateCommentTest(
|
||||
ForumsEnableMixin,
|
||||
UrlResetMixin,
|
||||
SharedModuleStoreTestCase,
|
||||
MockSignalHandlerMixin,
|
||||
ForumMockUtilsMixin,
|
||||
):
|
||||
"""Tests for create_comment"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
super().setUpClassAndForumMock()
|
||||
cls.course = CourseFactory.create()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
@mock.patch.dict(
|
||||
"django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}
|
||||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
httpretty.reset()
|
||||
httpretty.enable()
|
||||
self.addCleanup(httpretty.reset)
|
||||
self.addCleanup(httpretty.disable)
|
||||
self.course = CourseFactory.create()
|
||||
self.user = UserFactory.create()
|
||||
self.register_get_user_response(self.user)
|
||||
self.request = RequestFactory().get("/test_path")
|
||||
self.request.user = self.user
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
self.register_get_thread_response(
|
||||
make_minimal_cs_thread(
|
||||
{
|
||||
"id": "test_thread",
|
||||
"course_id": str(self.course.id),
|
||||
"commentable_id": "test_topic",
|
||||
}
|
||||
)
|
||||
)
|
||||
self.minimal_data = {
|
||||
"thread_id": "test_thread",
|
||||
"raw_body": "Test body",
|
||||
}
|
||||
|
||||
mock_response = {
|
||||
"collection": [],
|
||||
"page": 1,
|
||||
"num_pages": 1,
|
||||
"subscriptions_count": 1,
|
||||
"corrected_text": None,
|
||||
}
|
||||
self.register_get_subscriptions("cohort_thread", mock_response)
|
||||
self.register_get_subscriptions("test_thread", mock_response)
|
||||
|
||||
def test_abuse_flagged(self):
|
||||
self.register_post_comment_response(
|
||||
{"id": "test_comment", "username": self.user.username}, "test_thread"
|
||||
)
|
||||
self.register_comment_flag_response("test_comment")
|
||||
data = self.minimal_data.copy()
|
||||
data["abuse_flagged"] = "True"
|
||||
result = create_comment(self.request, data)
|
||||
assert result["abuse_flagged"] is True
|
||||
|
||||
self.check_mock_called("update_comment_flag")
|
||||
params = {
|
||||
"comment_id": "test_comment",
|
||||
"action": "flag",
|
||||
"user_id": "1",
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
self.check_mock_called_with("update_comment_flag", -1, **params)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(api, "thread_edited")
|
||||
@disable_signal(api, "thread_voted")
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class UpdateThreadTest(
|
||||
ForumsEnableMixin,
|
||||
UrlResetMixin,
|
||||
SharedModuleStoreTestCase,
|
||||
MockSignalHandlerMixin,
|
||||
ForumMockUtilsMixin,
|
||||
):
|
||||
"""Tests for update_thread"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
super().setUpClassAndForumMock()
|
||||
cls.course = CourseFactory.create()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
@mock.patch.dict(
|
||||
"django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}
|
||||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
httpretty.reset()
|
||||
httpretty.enable()
|
||||
self.addCleanup(httpretty.reset)
|
||||
self.addCleanup(httpretty.disable)
|
||||
self.user = UserFactory.create()
|
||||
self.register_get_user_response(self.user)
|
||||
self.request = RequestFactory().get("/test_path")
|
||||
self.request.user = self.user
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
|
||||
def register_thread(self, overrides=None):
|
||||
"""
|
||||
Make a thread with appropriate data overridden by the overrides
|
||||
parameter and register mock responses for both GET and PUT on its
|
||||
endpoint.
|
||||
"""
|
||||
cs_data = make_minimal_cs_thread(
|
||||
{
|
||||
"id": "test_thread",
|
||||
"course_id": str(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_data.update(overrides or {})
|
||||
self.register_get_thread_response(cs_data)
|
||||
self.register_put_thread_response(cs_data)
|
||||
|
||||
@ddt.data(*itertools.product([True, False], [True, False]))
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_abuse_flagged(self, old_flagged, new_flagged, mock_emit):
|
||||
"""
|
||||
Test attempts to edit the "abuse_flagged" field.
|
||||
|
||||
old_flagged indicates whether the thread should be flagged at the start
|
||||
of the test. new_flagged indicates the value for the "abuse_flagged"
|
||||
field in the update. If old_flagged and new_flagged are the same, no
|
||||
update should be made. Otherwise, a PUT should be made to the flag or
|
||||
or unflag endpoint according to the new_flagged value.
|
||||
"""
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread_flag_response("test_thread")
|
||||
self.register_thread(
|
||||
{"abuse_flaggers": [str(self.user.id)] if old_flagged else []}
|
||||
)
|
||||
data = {"abuse_flagged": new_flagged}
|
||||
result = update_thread(self.request, "test_thread", data)
|
||||
assert result["abuse_flagged"] == new_flagged
|
||||
|
||||
flag_func_calls = self.get_mock_func_calls("update_thread_flag")
|
||||
last_function_args = flag_func_calls[-1] if flag_func_calls else None
|
||||
|
||||
if old_flagged == new_flagged:
|
||||
assert last_function_args is None
|
||||
else:
|
||||
assert last_function_args[1]["action"] == (
|
||||
"flag" if new_flagged else "unflag"
|
||||
)
|
||||
params = {
|
||||
"thread_id": "test_thread",
|
||||
"action": "flag" if new_flagged else "unflag",
|
||||
"user_id": "1",
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
if not new_flagged:
|
||||
params["update_all"] = False
|
||||
self.check_mock_called_with("update_thread_flag", -1, **params)
|
||||
|
||||
expected_event_name = (
|
||||
"edx.forum.thread.reported"
|
||||
if new_flagged
|
||||
else "edx.forum.thread.unreported"
|
||||
)
|
||||
expected_event_data = {
|
||||
"body": "Original body",
|
||||
"id": "test_thread",
|
||||
"content_type": "Post",
|
||||
"commentable_id": "original_topic",
|
||||
"url": "",
|
||||
"user_course_roles": [],
|
||||
"user_forums_roles": [FORUM_ROLE_STUDENT],
|
||||
"target_username": self.user.username,
|
||||
"title_truncated": False,
|
||||
"title": "Original Title",
|
||||
"thread_type": "discussion",
|
||||
"group_id": None,
|
||||
"truncated": False,
|
||||
}
|
||||
if not new_flagged:
|
||||
expected_event_data["reported_status_cleared"] = False
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
@ddt.data(
|
||||
(False, True),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_thread_un_abuse_flag_for_moderator_role(
|
||||
self, is_author, remove_all, mock_emit
|
||||
):
|
||||
"""
|
||||
Test un-abuse flag for moderator role.
|
||||
|
||||
When moderator unflags a reported thread, it should
|
||||
pass the "all" flag to the api. This will indicate
|
||||
to the api to clear all abuse_flaggers, and mark the
|
||||
thread as unreported.
|
||||
"""
|
||||
_assign_role_to_user(
|
||||
user=self.user, course_id=self.course.id, role=FORUM_ROLE_ADMINISTRATOR
|
||||
)
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread_flag_response("test_thread")
|
||||
self.register_thread(
|
||||
{
|
||||
"abuse_flaggers": ["11"],
|
||||
"user_id": str(self.user.id) if is_author else "12",
|
||||
}
|
||||
)
|
||||
data = {"abuse_flagged": False}
|
||||
update_thread(self.request, "test_thread", data)
|
||||
|
||||
params = {
|
||||
"thread_id": "test_thread",
|
||||
"action": "unflag",
|
||||
"user_id": "1",
|
||||
"update_all": True if remove_all else False,
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
|
||||
self.check_mock_called_with("update_thread_flag", -1, **params)
|
||||
|
||||
expected_event_name = "edx.forum.thread.unreported"
|
||||
expected_event_data = {
|
||||
"body": "Original body",
|
||||
"id": "test_thread",
|
||||
"content_type": "Post",
|
||||
"commentable_id": "original_topic",
|
||||
"url": "",
|
||||
"user_course_roles": [],
|
||||
"user_forums_roles": [FORUM_ROLE_STUDENT, FORUM_ROLE_ADMINISTRATOR],
|
||||
"target_username": self.user.username,
|
||||
"title_truncated": False,
|
||||
"title": "Original Title",
|
||||
"reported_status_cleared": False,
|
||||
"thread_type": "discussion",
|
||||
"group_id": None,
|
||||
"truncated": False,
|
||||
}
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(api, "comment_edited")
|
||||
@disable_signal(api, "comment_voted")
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class UpdateCommentTest(
|
||||
ForumsEnableMixin,
|
||||
UrlResetMixin,
|
||||
SharedModuleStoreTestCase,
|
||||
MockSignalHandlerMixin,
|
||||
ForumMockUtilsMixin,
|
||||
):
|
||||
"""Tests for update_comment"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
super().setUpClassAndForumMock()
|
||||
cls.course = CourseFactory.create()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
"""Stop patches after tests complete."""
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
@mock.patch.dict(
|
||||
"django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True}
|
||||
)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
httpretty.reset()
|
||||
httpretty.enable()
|
||||
self.addCleanup(httpretty.reset)
|
||||
self.addCleanup(httpretty.disable)
|
||||
self.user = UserFactory.create()
|
||||
self.register_get_user_response(self.user)
|
||||
self.request = RequestFactory().get("/test_path")
|
||||
self.request.user = self.user
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
|
||||
def register_comment(self, overrides=None, thread_overrides=None, course=None):
|
||||
"""
|
||||
Make a comment with appropriate data overridden by the overrides
|
||||
parameter and register mock responses for both GET and PUT on its
|
||||
endpoint. Also mock GET for the related thread with thread_overrides.
|
||||
"""
|
||||
if course is None:
|
||||
course = self.course
|
||||
|
||||
cs_thread_data = make_minimal_cs_thread(
|
||||
{"id": "test_thread", "course_id": str(course.id)}
|
||||
)
|
||||
cs_thread_data.update(thread_overrides or {})
|
||||
self.register_get_thread_response(cs_thread_data)
|
||||
cs_comment_data = make_minimal_cs_comment(
|
||||
{
|
||||
"id": "test_comment",
|
||||
"course_id": cs_thread_data["course_id"],
|
||||
"thread_id": cs_thread_data["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",
|
||||
}
|
||||
)
|
||||
cs_comment_data.update(overrides or {})
|
||||
self.register_get_comment_response(cs_comment_data)
|
||||
self.register_put_comment_response(cs_comment_data)
|
||||
|
||||
@ddt.data(*itertools.product([True, False], [True, False]))
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_abuse_flagged(self, old_flagged, new_flagged, mock_emit):
|
||||
"""
|
||||
Test attempts to edit the "abuse_flagged" field.
|
||||
|
||||
old_flagged indicates whether the comment should be flagged at the start
|
||||
of the test. new_flagged indicates the value for the "abuse_flagged"
|
||||
field in the update. If old_flagged and new_flagged are the same, no
|
||||
update should be made. Otherwise, a PUT should be made to the flag or
|
||||
or unflag endpoint according to the new_flagged value.
|
||||
"""
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_comment_flag_response("test_comment")
|
||||
self.register_comment(
|
||||
{"abuse_flaggers": [str(self.user.id)] if old_flagged else []}
|
||||
)
|
||||
data = {"abuse_flagged": new_flagged}
|
||||
result = update_comment(self.request, "test_comment", data)
|
||||
assert result["abuse_flagged"] == new_flagged
|
||||
flag_func_calls = self.get_mock_func_calls("update_comment_flag")
|
||||
last_function_args = flag_func_calls[-1] if flag_func_calls else None
|
||||
|
||||
if old_flagged == new_flagged:
|
||||
assert last_function_args is None
|
||||
else:
|
||||
assert last_function_args[1]["action"] == (
|
||||
"flag" if new_flagged else "unflag"
|
||||
)
|
||||
params = {
|
||||
"comment_id": "test_comment",
|
||||
"action": "flag" if new_flagged else "unflag",
|
||||
"user_id": "1",
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
if not new_flagged:
|
||||
params["update_all"] = False
|
||||
self.check_mock_called_with("update_comment_flag", -1, **params)
|
||||
|
||||
expected_event_name = (
|
||||
"edx.forum.response.reported"
|
||||
if new_flagged
|
||||
else "edx.forum.response.unreported"
|
||||
)
|
||||
expected_event_data = {
|
||||
"body": "Original body",
|
||||
"id": "test_comment",
|
||||
"content_type": "Response",
|
||||
"commentable_id": "dummy",
|
||||
"url": "",
|
||||
"truncated": False,
|
||||
"user_course_roles": [],
|
||||
"user_forums_roles": [FORUM_ROLE_STUDENT],
|
||||
"target_username": self.user.username,
|
||||
}
|
||||
if not new_flagged:
|
||||
expected_event_data["reported_status_cleared"] = False
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
|
||||
@ddt.data(
|
||||
(False, True),
|
||||
(True, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
@mock.patch("eventtracking.tracker.emit")
|
||||
def test_comment_un_abuse_flag_for_moderator_role(
|
||||
self, is_author, remove_all, mock_emit
|
||||
):
|
||||
"""
|
||||
Test un-abuse flag for moderator role.
|
||||
|
||||
When moderator unflags a reported comment, it should
|
||||
pass the "all" flag to the api. This will indicate
|
||||
to the api to clear all abuse_flaggers, and mark the
|
||||
comment as unreported.
|
||||
"""
|
||||
_assign_role_to_user(
|
||||
user=self.user, course_id=self.course.id, role=FORUM_ROLE_ADMINISTRATOR
|
||||
)
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_comment_flag_response("test_comment")
|
||||
self.register_comment(
|
||||
{
|
||||
"abuse_flaggers": ["11"],
|
||||
"user_id": str(self.user.id) if is_author else "12",
|
||||
}
|
||||
)
|
||||
data = {"abuse_flagged": False}
|
||||
update_comment(self.request, "test_comment", data)
|
||||
|
||||
params = {
|
||||
"comment_id": "test_comment",
|
||||
"action": "unflag",
|
||||
"user_id": "1",
|
||||
"update_all": True if remove_all else False,
|
||||
"course_id": str(self.course.id),
|
||||
}
|
||||
self.check_mock_called_with("update_comment_flag", -1, **params)
|
||||
|
||||
expected_event_name = "edx.forum.response.unreported"
|
||||
expected_event_data = {
|
||||
"body": "Original body",
|
||||
"id": "test_comment",
|
||||
"content_type": "Response",
|
||||
"commentable_id": "dummy",
|
||||
"truncated": False,
|
||||
"url": "",
|
||||
"user_course_roles": [],
|
||||
"user_forums_roles": [FORUM_ROLE_STUDENT, FORUM_ROLE_ADMINISTRATOR],
|
||||
"target_username": self.user.username,
|
||||
"reported_status_cleared": False,
|
||||
}
|
||||
|
||||
actual_event_name, actual_event_data = mock_emit.call_args[0]
|
||||
self.assertEqual(actual_event_name, expected_event_name)
|
||||
self.assertEqual(actual_event_data, expected_event_data)
|
||||
@@ -18,7 +18,6 @@ from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from rest_framework import status
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.test import APIClient, APITestCase
|
||||
|
||||
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
|
||||
@@ -38,7 +37,7 @@ from common.djangoapps.student.tests.factories import (
|
||||
SuperuserFactory,
|
||||
UserFactory
|
||||
)
|
||||
from common.djangoapps.util.testing import PatchMediaTypeMixin, UrlResetMixin
|
||||
from common.djangoapps.util.testing import UrlResetMixin
|
||||
from common.test.utils import disable_signal
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.utils import (
|
||||
ForumsEnableMixin,
|
||||
@@ -1481,162 +1480,6 @@ class ThreadViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
||||
assert response_data == expected_response_data
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@httpretty.activate
|
||||
@disable_signal(api, 'thread_edited')
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
|
||||
"""Tests for ThreadViewSet partial_update"""
|
||||
|
||||
def setUp(self):
|
||||
self.unsupported_media_type = JSONParser.media_type
|
||||
super().setUp()
|
||||
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
|
||||
patcher = mock.patch(
|
||||
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
|
||||
return_value=False
|
||||
)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
patcher = mock.patch(
|
||||
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
|
||||
)
|
||||
self.mock_get_course_id_by_thread = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def test_basic(self):
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread({
|
||||
"created_at": "Test Created Date",
|
||||
"updated_at": "Test Updated Date",
|
||||
"read": True,
|
||||
"resp_total": 2,
|
||||
})
|
||||
request_data = {"raw_body": "Edited body"}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'raw_body': 'Edited body',
|
||||
'rendered_body': '<p>Edited body</p>',
|
||||
'preview_body': 'Edited body',
|
||||
'editable_fields': [
|
||||
'abuse_flagged', 'anonymous', 'copy_link', 'following', 'raw_body', 'read',
|
||||
'title', 'topic_id', 'type'
|
||||
],
|
||||
'created_at': 'Test Created Date',
|
||||
'updated_at': 'Test Updated Date',
|
||||
'comment_count': 1,
|
||||
'read': True,
|
||||
'response_count': 2,
|
||||
})
|
||||
assert parsed_body(httpretty.last_request()) == {
|
||||
'course_id': [str(self.course.id)],
|
||||
'commentable_id': ['test_topic'],
|
||||
'thread_type': ['discussion'],
|
||||
'title': ['Test Title'],
|
||||
'body': ['Edited body'],
|
||||
'user_id': [str(self.user.id)],
|
||||
'anonymous': ['False'],
|
||||
'anonymous_to_peers': ['False'],
|
||||
'closed': ['False'],
|
||||
'pinned': ['False'],
|
||||
'read': ['True'],
|
||||
'editing_user_id': [str(self.user.id)],
|
||||
}
|
||||
|
||||
def test_error(self):
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread()
|
||||
request_data = {"title": ""}
|
||||
response = self.request_patch(request_data)
|
||||
expected_response_data = {
|
||||
"field_errors": {"title": {"developer_message": "This field may not be blank."}}
|
||||
}
|
||||
assert response.status_code == 400
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert 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, "read": True})
|
||||
self.register_flag_response("thread", "test_thread")
|
||||
request_data = {field: value}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'read': True,
|
||||
'closed': True,
|
||||
'abuse_flagged': value,
|
||||
'editable_fields': ['abuse_flagged', 'copy_link', 'read'],
|
||||
'comment_count': 1, 'unread_comment_count': 0
|
||||
})
|
||||
|
||||
@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)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_patch_read_owner_user(self):
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread({"resp_total": 2})
|
||||
self.register_read_response(self.user, "thread", "test_thread")
|
||||
request_data = {"read": True}
|
||||
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'comment_count': 1,
|
||||
'read': True,
|
||||
'editable_fields': [
|
||||
'abuse_flagged', 'anonymous', 'copy_link', 'following', 'raw_body', 'read',
|
||||
'title', 'topic_id', 'type'
|
||||
],
|
||||
'response_count': 2
|
||||
})
|
||||
|
||||
def test_patch_read_non_owner_user(self):
|
||||
self.register_get_user_response(self.user)
|
||||
thread_owner_user = UserFactory.create(password=self.password)
|
||||
CourseEnrollmentFactory.create(user=thread_owner_user, course_id=self.course.id)
|
||||
self.register_get_user_response(thread_owner_user)
|
||||
self.register_thread({
|
||||
"username": thread_owner_user.username,
|
||||
"user_id": str(thread_owner_user.id),
|
||||
"resp_total": 2,
|
||||
})
|
||||
self.register_read_response(self.user, "thread", "test_thread")
|
||||
|
||||
request_data = {"read": True}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'author': str(thread_owner_user.username),
|
||||
'comment_count': 1,
|
||||
'can_delete': False,
|
||||
'read': True,
|
||||
'editable_fields': ['abuse_flagged', 'copy_link', 'following', 'read', 'voted'],
|
||||
'response_count': 2
|
||||
})
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
@disable_signal(api, 'thread_deleted')
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
@@ -2632,148 +2475,6 @@ class CommentViewSetCreateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(api, 'comment_edited')
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
|
||||
"""Tests for CommentViewSet partial_update"""
|
||||
|
||||
def setUp(self):
|
||||
self.unsupported_media_type = JSONParser.media_type
|
||||
super().setUp()
|
||||
httpretty.reset()
|
||||
httpretty.enable()
|
||||
self.addCleanup(httpretty.reset)
|
||||
self.addCleanup(httpretty.disable)
|
||||
patcher = mock.patch(
|
||||
'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled',
|
||||
return_value=False
|
||||
)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
patcher = mock.patch(
|
||||
"openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment"
|
||||
)
|
||||
self.mock_get_course_id_by_comment = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
patcher = mock.patch(
|
||||
"openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread"
|
||||
)
|
||||
self.mock_get_course_id_by_thread = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
self.register_get_user_response(self.user)
|
||||
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
|
||||
|
||||
def expected_response_data(self, overrides=None):
|
||||
"""
|
||||
create expected response data from comment update endpoint
|
||||
"""
|
||||
response_data = {
|
||||
"id": "test_comment",
|
||||
"thread_id": "test_thread",
|
||||
"parent_id": None,
|
||||
"author": self.user.username,
|
||||
"author_label": None,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"updated_at": "1970-01-01T00:00:00Z",
|
||||
"raw_body": "Original body",
|
||||
"rendered_body": "<p>Original body</p>",
|
||||
"endorsed": False,
|
||||
"endorsed_by": None,
|
||||
"endorsed_by_label": None,
|
||||
"endorsed_at": None,
|
||||
"abuse_flagged": False,
|
||||
"abuse_flagged_any_user": None,
|
||||
"voted": False,
|
||||
"vote_count": 0,
|
||||
"children": [],
|
||||
"editable_fields": [],
|
||||
"child_count": 0,
|
||||
"can_delete": True,
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"last_edit": None,
|
||||
"edit_by_label": None,
|
||||
"profile_image": {
|
||||
"has_image": False,
|
||||
"image_url_full": "http://testserver/static/default_500.png",
|
||||
"image_url_large": "http://testserver/static/default_120.png",
|
||||
"image_url_medium": "http://testserver/static/default_50.png",
|
||||
"image_url_small": "http://testserver/static/default_30.png",
|
||||
},
|
||||
}
|
||||
response_data.update(overrides or {})
|
||||
return response_data
|
||||
|
||||
def test_basic(self):
|
||||
self.register_thread()
|
||||
self.register_comment({"created_at": "Test Created Date", "updated_at": "Test Updated Date"})
|
||||
request_data = {"raw_body": "Edited body"}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_response_data({
|
||||
'raw_body': 'Edited body',
|
||||
'rendered_body': '<p>Edited body</p>',
|
||||
'editable_fields': ['abuse_flagged', 'anonymous', 'raw_body'],
|
||||
'created_at': 'Test Created Date',
|
||||
'updated_at': 'Test Updated Date'
|
||||
})
|
||||
assert parsed_body(httpretty.last_request()) == {
|
||||
'body': ['Edited body'],
|
||||
'course_id': [str(self.course.id)],
|
||||
'user_id': [str(self.user.id)],
|
||||
'anonymous': ['False'],
|
||||
'anonymous_to_peers': ['False'],
|
||||
'endorsed': ['False'],
|
||||
'editing_user_id': [str(self.user.id)],
|
||||
}
|
||||
|
||||
def test_error(self):
|
||||
self.register_thread()
|
||||
self.register_comment()
|
||||
request_data = {"raw_body": ""}
|
||||
response = self.request_patch(request_data)
|
||||
expected_response_data = {
|
||||
"field_errors": {"raw_body": {"developer_message": "This field may not be blank."}}
|
||||
}
|
||||
assert response.status_code == 400
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert 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)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_response_data({
|
||||
'abuse_flagged': value,
|
||||
"abuse_flagged_any_user": None,
|
||||
'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)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
@httpretty.activate
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class ThreadViewSetRetrieveTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, ProfileImageTestMixin):
|
||||
|
||||
444
lms/djangoapps/discussion/rest_api/tests/test_views_v2.py
Normal file
444
lms/djangoapps/discussion/rest_api/tests/test_views_v2.py
Normal file
@@ -0,0 +1,444 @@
|
||||
# pylint: skip-file
|
||||
"""
|
||||
Tests for the external REST API endpoints of the Discussion API (views_v2.py).
|
||||
|
||||
This module focuses on integration tests for the Django REST Framework views that expose the Discussion API.
|
||||
It verifies the correct behavior of the API endpoints, including authentication, permissions, request/response formats,
|
||||
and integration with the underlying discussion service. These tests ensure that the endpoints correctly handle
|
||||
various user roles, input data, and edge cases, and that they return appropriate HTTP status codes and response bodies.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime
|
||||
from unittest import mock
|
||||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
import ddt
|
||||
import httpretty
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.mixins import MockForumApiMixin
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from pytz import UTC
|
||||
from rest_framework import status
|
||||
from rest_framework.parsers import JSONParser
|
||||
from rest_framework.test import APIClient, APITestCase
|
||||
|
||||
from lms.djangoapps.discussion.toggles import ENABLE_DISCUSSIONS_MFE
|
||||
from lms.djangoapps.discussion.rest_api.utils import get_usernames_from_search_string
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase, SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory, check_mongo_calls
|
||||
|
||||
from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.models import get_retired_username_by_username, CourseEnrollment
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole, GlobalStaff
|
||||
from common.djangoapps.student.tests.factories import (
|
||||
AdminFactory,
|
||||
CourseEnrollmentFactory,
|
||||
SuperuserFactory,
|
||||
UserFactory
|
||||
)
|
||||
from common.djangoapps.util.testing import PatchMediaTypeMixin, UrlResetMixin
|
||||
from common.test.utils import disable_signal
|
||||
from lms.djangoapps.discussion.tests.utils import (
|
||||
make_minimal_cs_comment,
|
||||
make_minimal_cs_thread,
|
||||
)
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.utils import (
|
||||
ForumsEnableMixin,
|
||||
config_course_discussions,
|
||||
topic_name_to_id,
|
||||
)
|
||||
from lms.djangoapps.discussion.rest_api import api
|
||||
from lms.djangoapps.discussion.rest_api.tests.utils import (
|
||||
CommentsServiceMockMixin,
|
||||
ForumMockUtilsMixin,
|
||||
ProfileImageTestMixin,
|
||||
make_paginated_api_response,
|
||||
parsed_body,
|
||||
)
|
||||
from openedx.core.djangoapps.course_groups.tests.helpers import config_course_cohorts
|
||||
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS
|
||||
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, DiscussionTopicLink, Provider
|
||||
from openedx.core.djangoapps.discussions.tasks import update_discussions_settings_from_course_task
|
||||
from openedx.core.djangoapps.django_comment_common.models import CourseDiscussionSettings, Role
|
||||
from openedx.core.djangoapps.django_comment_common.utils import seed_permissions_roles
|
||||
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
|
||||
from openedx.core.djangoapps.oauth_dispatch.tests.factories import AccessTokenFactory, ApplicationFactory
|
||||
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_storage
|
||||
from openedx.core.djangoapps.user_api.models import RetirementState, UserRetirementStatus
|
||||
|
||||
|
||||
class DiscussionAPIViewTestMixin(ForumsEnableMixin, ForumMockUtilsMixin, UrlResetMixin):
|
||||
"""
|
||||
Mixin for common code in tests of Discussion API views. This includes
|
||||
creation of common structures (e.g. a course, user, and enrollment), logging
|
||||
in the test client, utility functions, and a test case for unauthenticated
|
||||
requests. Subclasses must set self.url in their setUp methods.
|
||||
"""
|
||||
|
||||
client_class = APIClient
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.maxDiff = None # pylint: disable=invalid-name
|
||||
self.course = CourseFactory.create(
|
||||
org="x",
|
||||
course="y",
|
||||
run="z",
|
||||
start=datetime.now(UTC),
|
||||
discussion_topics={"Test Topic": {"id": "test_topic"}}
|
||||
)
|
||||
self.password = "Password1234"
|
||||
self.user = UserFactory.create(password=self.password)
|
||||
# Ensure that parental controls don't apply to this user
|
||||
self.user.profile.year_of_birth = 1970
|
||||
self.user.profile.save()
|
||||
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id)
|
||||
self.client.login(username=self.user.username, password=self.password)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
super().setUpClassAndForumMock()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
super().disposeForumMocks()
|
||||
|
||||
def assert_response_correct(self, response, expected_status, expected_content):
|
||||
"""
|
||||
Assert that the response has the given status code and parsed content
|
||||
"""
|
||||
assert response.status_code == expected_status
|
||||
parsed_content = json.loads(response.content.decode('utf-8'))
|
||||
assert 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": str(self.course.id),
|
||||
"commentable_id": "test_topic",
|
||||
"username": self.user.username,
|
||||
"user_id": str(self.user.id),
|
||||
"thread_type": "discussion",
|
||||
"title": "Test Title",
|
||||
"body": "Test 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": str(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):
|
||||
self.client.logout()
|
||||
response = self.client.get(self.url)
|
||||
self.assert_response_correct(
|
||||
response,
|
||||
401,
|
||||
{"developer_message": "Authentication credentials were not provided."}
|
||||
)
|
||||
|
||||
def test_inactive(self):
|
||||
self.user.is_active = False
|
||||
self.test_basic()
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@httpretty.activate
|
||||
@disable_signal(api, 'thread_edited')
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class ThreadViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
|
||||
"""Tests for ThreadViewSet partial_update"""
|
||||
|
||||
def setUp(self):
|
||||
self.unsupported_media_type = JSONParser.media_type
|
||||
super().setUp()
|
||||
self.url = reverse("thread-detail", kwargs={"thread_id": "test_thread"})
|
||||
|
||||
def test_basic(self):
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread({
|
||||
"created_at": "Test Created Date",
|
||||
"updated_at": "Test Updated Date",
|
||||
"read": True,
|
||||
"resp_total": 2,
|
||||
})
|
||||
request_data = {"raw_body": "Edited body"}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'raw_body': 'Edited body',
|
||||
'rendered_body': '<p>Edited body</p>',
|
||||
'preview_body': 'Edited body',
|
||||
'editable_fields': [
|
||||
'abuse_flagged', 'anonymous', 'copy_link', 'following', 'raw_body', 'read',
|
||||
'title', 'topic_id', 'type'
|
||||
],
|
||||
'created_at': 'Test Created Date',
|
||||
'updated_at': 'Test Updated Date',
|
||||
'comment_count': 1,
|
||||
'read': True,
|
||||
'response_count': 2,
|
||||
})
|
||||
|
||||
params = {
|
||||
'thread_id': 'test_thread',
|
||||
'course_id': str(self.course.id),
|
||||
'commentable_id': 'test_topic',
|
||||
'thread_type': 'discussion',
|
||||
'title': 'Test Title',
|
||||
'body': 'Edited body',
|
||||
'user_id': str(self.user.id),
|
||||
'anonymous': False,
|
||||
'anonymous_to_peers': False,
|
||||
'closed': False,
|
||||
'pinned': False,
|
||||
'read': True,
|
||||
'editing_user_id': str(self.user.id),
|
||||
}
|
||||
self.check_mock_called_with('update_thread', -1, **params)
|
||||
|
||||
def test_error(self):
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread()
|
||||
request_data = {"title": ""}
|
||||
response = self.request_patch(request_data)
|
||||
expected_response_data = {
|
||||
"field_errors": {"title": {"developer_message": "This field may not be blank."}}
|
||||
}
|
||||
assert response.status_code == 400
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert 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, "read": True})
|
||||
self.register_flag_response("thread", "test_thread")
|
||||
request_data = {field: value}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'read': True,
|
||||
'closed': True,
|
||||
'abuse_flagged': value,
|
||||
'editable_fields': ['abuse_flagged', 'copy_link', 'read'],
|
||||
'comment_count': 1, 'unread_comment_count': 0
|
||||
})
|
||||
|
||||
@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)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_patch_read_owner_user(self):
|
||||
self.register_get_user_response(self.user)
|
||||
self.register_thread({"resp_total": 2})
|
||||
self.register_read_response(self.user, "thread", "test_thread")
|
||||
request_data = {"read": True}
|
||||
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_thread_data({
|
||||
'comment_count': 1,
|
||||
'read': True,
|
||||
'editable_fields': [
|
||||
'abuse_flagged', 'anonymous', 'copy_link', 'following', 'raw_body', 'read',
|
||||
'title', 'topic_id', 'type'
|
||||
],
|
||||
'response_count': 2
|
||||
})
|
||||
|
||||
def test_patch_read_non_owner_user(self):
|
||||
self.register_get_user_response(self.user)
|
||||
thread_owner_user = UserFactory.create(password=self.password)
|
||||
CourseEnrollmentFactory.create(user=thread_owner_user, course_id=self.course.id)
|
||||
self.register_thread({
|
||||
"username": thread_owner_user.username,
|
||||
"user_id": str(thread_owner_user.id),
|
||||
"resp_total": 2,
|
||||
})
|
||||
self.register_read_response(self.user, "thread", "test_thread")
|
||||
|
||||
request_data = {"read": True}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
expected_data = self.expected_thread_data({
|
||||
'author': str(thread_owner_user.username),
|
||||
'comment_count': 1,
|
||||
'can_delete': False,
|
||||
'read': True,
|
||||
'editable_fields': ['abuse_flagged', 'copy_link', 'following', 'read', 'voted'],
|
||||
'response_count': 2
|
||||
})
|
||||
assert response_data == expected_data
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@disable_signal(api, 'comment_edited')
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
|
||||
class CommentViewSetPartialUpdateTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, PatchMediaTypeMixin):
|
||||
"""Tests for CommentViewSet partial_update"""
|
||||
|
||||
def setUp(self):
|
||||
self.unsupported_media_type = JSONParser.media_type
|
||||
super().setUp()
|
||||
self.register_get_user_response(self.user)
|
||||
self.url = reverse("comment-detail", kwargs={"comment_id": "test_comment"})
|
||||
|
||||
def expected_response_data(self, overrides=None):
|
||||
"""
|
||||
create expected response data from comment update endpoint
|
||||
"""
|
||||
response_data = {
|
||||
"id": "test_comment",
|
||||
"thread_id": "test_thread",
|
||||
"parent_id": None,
|
||||
"author": self.user.username,
|
||||
"author_label": None,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"updated_at": "1970-01-01T00:00:00Z",
|
||||
"raw_body": "Original body",
|
||||
"rendered_body": "<p>Original body</p>",
|
||||
"endorsed": False,
|
||||
"endorsed_by": None,
|
||||
"endorsed_by_label": None,
|
||||
"endorsed_at": None,
|
||||
"abuse_flagged": False,
|
||||
"abuse_flagged_any_user": None,
|
||||
"voted": False,
|
||||
"vote_count": 0,
|
||||
"children": [],
|
||||
"editable_fields": [],
|
||||
"child_count": 0,
|
||||
"can_delete": True,
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"last_edit": None,
|
||||
"edit_by_label": None,
|
||||
"profile_image": {
|
||||
"has_image": False,
|
||||
"image_url_full": "http://testserver/static/default_500.png",
|
||||
"image_url_large": "http://testserver/static/default_120.png",
|
||||
"image_url_medium": "http://testserver/static/default_50.png",
|
||||
"image_url_small": "http://testserver/static/default_30.png",
|
||||
},
|
||||
}
|
||||
response_data.update(overrides or {})
|
||||
return response_data
|
||||
|
||||
def test_basic(self):
|
||||
self.register_thread()
|
||||
self.register_comment({"created_at": "Test Created Date", "updated_at": "Test Updated Date"})
|
||||
request_data = {"raw_body": "Edited body"}
|
||||
response = self.request_patch(request_data)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_response_data({
|
||||
'raw_body': 'Edited body',
|
||||
'rendered_body': '<p>Edited body</p>',
|
||||
'editable_fields': ['abuse_flagged', 'anonymous', 'raw_body'],
|
||||
'created_at': 'Test Created Date',
|
||||
'updated_at': 'Test Updated Date'
|
||||
})
|
||||
params = {
|
||||
'comment_id': 'test_comment',
|
||||
'body': 'Edited body',
|
||||
'course_id': str(self.course.id),
|
||||
'user_id': str(self.user.id),
|
||||
'anonymous': False,
|
||||
'anonymous_to_peers': False,
|
||||
'endorsed': False,
|
||||
'editing_user_id': str(self.user.id),
|
||||
}
|
||||
self.check_mock_called_with('update_comment', -1, **params)
|
||||
|
||||
def test_error(self):
|
||||
self.register_thread()
|
||||
self.register_comment()
|
||||
request_data = {"raw_body": ""}
|
||||
response = self.request_patch(request_data)
|
||||
expected_response_data = {
|
||||
"field_errors": {"raw_body": {"developer_message": "This field may not be blank."}}
|
||||
}
|
||||
assert response.status_code == 400
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert 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)
|
||||
assert response.status_code == 200
|
||||
response_data = json.loads(response.content.decode('utf-8'))
|
||||
assert response_data == self.expected_response_data({
|
||||
'abuse_flagged': value,
|
||||
"abuse_flagged_any_user": None,
|
||||
'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)
|
||||
assert response.status_code == 400
|
||||
@@ -14,6 +14,7 @@ import httpretty
|
||||
from PIL import Image
|
||||
from pytz import UTC
|
||||
|
||||
from lms.djangoapps.discussion.django_comment_client.tests.mixins import MockForumApiMixin
|
||||
from openedx.core.djangoapps.profile_images.images import create_profile_images
|
||||
from openedx.core.djangoapps.profile_images.tests.helpers import make_image_file
|
||||
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_names, set_has_profile_image
|
||||
@@ -51,6 +52,34 @@ def _get_thread_callback(thread_data):
|
||||
return callback
|
||||
|
||||
|
||||
def make_thread_callback(thread_data):
|
||||
"""
|
||||
Returns a function that simulates thread creation/update behavior,
|
||||
applying overrides based on keyword arguments (e.g., mock request body).
|
||||
"""
|
||||
|
||||
def callback(*args, **kwargs):
|
||||
# Simulate default thread response
|
||||
response_data = make_minimal_cs_thread(thread_data)
|
||||
original_data = response_data.copy()
|
||||
|
||||
for key, val in kwargs.items():
|
||||
if key in ["anonymous", "anonymous_to_peers", "closed", "pinned"]:
|
||||
response_data[key] = val is True or val == "True"
|
||||
elif key == "edit_reason_code":
|
||||
response_data["edit_history"] = [{
|
||||
"original_body": original_data["body"],
|
||||
"author": thread_data.get("username"),
|
||||
"reason_code": val,
|
||||
}]
|
||||
else:
|
||||
response_data[key] = val
|
||||
|
||||
return response_data
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
def _get_comment_callback(comment_data, thread_id, parent_id):
|
||||
"""
|
||||
Get a callback function that will return a comment containing the given data
|
||||
@@ -86,6 +115,38 @@ def _get_comment_callback(comment_data, thread_id, parent_id):
|
||||
return callback
|
||||
|
||||
|
||||
def make_comment_callback(comment_data, thread_id, parent_id):
|
||||
"""
|
||||
Returns a callable that mimics comment creation or update behavior,
|
||||
applying overrides based on keyword arguments like a parsed request body.
|
||||
"""
|
||||
|
||||
def callback(*args, **kwargs):
|
||||
response_data = make_minimal_cs_comment(comment_data)
|
||||
original_data = response_data.copy()
|
||||
|
||||
# Inject thread_id and parent_id
|
||||
response_data["thread_id"] = thread_id
|
||||
response_data["parent_id"] = parent_id
|
||||
|
||||
# Override fields based on "incoming request"
|
||||
for key, val in kwargs.items():
|
||||
if key in ["anonymous", "anonymous_to_peers", "endorsed"]:
|
||||
response_data[key] = val is True or val == "True"
|
||||
elif key == "edit_reason_code":
|
||||
response_data["edit_history"] = [{
|
||||
"original_body": original_data["body"],
|
||||
"author": comment_data.get("username"),
|
||||
"reason_code": val,
|
||||
}]
|
||||
else:
|
||||
response_data[key] = val
|
||||
|
||||
return response_data
|
||||
|
||||
return callback
|
||||
|
||||
|
||||
class CommentsServiceMockMixin:
|
||||
"""Mixin with utility methods for mocking the comments service"""
|
||||
|
||||
@@ -521,6 +582,226 @@ class CommentsServiceMockMixin:
|
||||
return response_data
|
||||
|
||||
|
||||
class ForumMockUtilsMixin(MockForumApiMixin):
|
||||
"""Mixin with utility methods for mocking the comments service"""
|
||||
|
||||
def register_get_threads_response(self, threads, page, num_pages):
|
||||
"""Register a mock response for GET on the CS thread list endpoint"""
|
||||
self.set_mock_return_value('get_user_threads', {
|
||||
"collection": threads,
|
||||
"page": page,
|
||||
"num_pages": num_pages,
|
||||
"thread_count": len(threads),
|
||||
})
|
||||
|
||||
def register_get_course_commentable_counts_response(self, course_id, thread_counts):
|
||||
"""Register a mock response for GET on the CS thread list endpoint"""
|
||||
self.set_mock_return_value('get_commentables_stats', thread_counts)
|
||||
|
||||
def register_get_threads_search_response(self, threads, rewrite, num_pages=1):
|
||||
"""Register a mock response for GET on the CS thread search endpoint"""
|
||||
self.set_mock_return_value('search_threads', {
|
||||
"collection": threads,
|
||||
"page": 1,
|
||||
"num_pages": num_pages,
|
||||
"corrected_text": rewrite,
|
||||
"thread_count": len(threads),
|
||||
})
|
||||
|
||||
def register_post_thread_response(self, thread_data):
|
||||
self.set_mock_side_effect('create_thread', make_thread_callback(thread_data))
|
||||
|
||||
def register_put_thread_response(self, thread_data):
|
||||
self.set_mock_side_effect('update_thread', make_thread_callback(thread_data))
|
||||
|
||||
def register_get_thread_error_response(self, thread_id, status_code):
|
||||
self.set_mock_return_value('get_thread', Exception(f"Error {status_code}"))
|
||||
|
||||
def register_get_thread_response(self, thread):
|
||||
self.set_mock_return_value('get_thread', thread)
|
||||
|
||||
def register_get_comments_response(self, comments, page, num_pages):
|
||||
self.set_mock_return_value('get_parent_comment', {
|
||||
"collection": comments,
|
||||
"page": page,
|
||||
"num_pages": num_pages,
|
||||
"comment_count": len(comments),
|
||||
})
|
||||
|
||||
def register_post_comment_response(self, comment_data, thread_id, parent_id=None):
|
||||
self.set_mock_side_effect(
|
||||
'create_child_comment' if parent_id else 'create_parent_comment',
|
||||
make_comment_callback(comment_data, thread_id, parent_id)
|
||||
)
|
||||
|
||||
def register_put_comment_response(self, comment_data):
|
||||
thread_id = comment_data["thread_id"]
|
||||
parent_id = comment_data.get("parent_id")
|
||||
self.set_mock_side_effect(
|
||||
'update_comment',
|
||||
make_comment_callback(comment_data, thread_id, parent_id)
|
||||
)
|
||||
|
||||
def register_get_comment_error_response(self, comment_id, status_code):
|
||||
self.set_mock_return_value('get_parent_comment', Exception(f"Error {status_code}"))
|
||||
|
||||
def register_get_comment_response(self, response_overrides):
|
||||
comment = make_minimal_cs_comment(response_overrides)
|
||||
self.set_mock_return_value('get_parent_comment', comment)
|
||||
|
||||
def register_get_user_response(self, user, subscribed_thread_ids=None, upvoted_ids=None):
|
||||
self.set_mock_return_value('get_user', {
|
||||
"id": str(user.id),
|
||||
"subscribed_thread_ids": subscribed_thread_ids or [],
|
||||
"upvoted_ids": upvoted_ids or [],
|
||||
})
|
||||
|
||||
def register_get_user_retire_response(self, user, body=""):
|
||||
self.set_mock_return_value('retire_user', body)
|
||||
|
||||
def register_get_username_replacement_response(self, user, status=200, body=""):
|
||||
self.set_mock_return_value('update_username', body)
|
||||
|
||||
def register_subscribed_threads_response(self, user, threads, page, num_pages):
|
||||
self.set_mock_return_value('get_user_subscriptions', {
|
||||
"collection": threads,
|
||||
"page": page,
|
||||
"num_pages": num_pages,
|
||||
"thread_count": len(threads),
|
||||
})
|
||||
|
||||
def register_course_stats_response(self, course_key, stats, page, num_pages):
|
||||
self.set_mock_return_value('get_user_course_stats', {
|
||||
"user_stats": stats,
|
||||
"page": page,
|
||||
"num_pages": num_pages,
|
||||
"count": len(stats),
|
||||
})
|
||||
|
||||
def register_subscription_response(self, user):
|
||||
self.set_mock_return_value('create_subscription', {})
|
||||
self.set_mock_return_value('delete_subscription', {})
|
||||
|
||||
def register_thread_votes_response(self, thread_id):
|
||||
self.set_mock_return_value('update_thread_votes', {})
|
||||
self.set_mock_return_value('delete_thread_vote', {})
|
||||
|
||||
def register_comment_votes_response(self, comment_id):
|
||||
self.set_mock_return_value('update_comment_votes', {})
|
||||
self.set_mock_return_value('delete_comment_vote', {})
|
||||
|
||||
def register_flag_response(self, content_type, content_id):
|
||||
if content_type == 'thread':
|
||||
self.set_mock_return_value('update_thread_flag', {})
|
||||
elif content_type == 'comment':
|
||||
self.set_mock_return_value('update_comment_flag', {})
|
||||
|
||||
def register_read_response(self, user, content_type, content_id):
|
||||
self.set_mock_return_value('mark_thread_as_read', {})
|
||||
|
||||
def register_delete_thread_response(self, thread_id):
|
||||
self.set_mock_return_value('delete_thread', {})
|
||||
|
||||
def register_delete_comment_response(self, comment_id):
|
||||
self.set_mock_return_value('delete_comment', {})
|
||||
|
||||
def register_user_active_threads(self, user_id, response):
|
||||
self.set_mock_return_value('get_user_active_threads', response)
|
||||
|
||||
def register_get_subscriptions(self, thread_id, response):
|
||||
self.set_mock_return_value('get_thread_subscriptions', response)
|
||||
|
||||
def register_thread_flag_response(self, thread_id):
|
||||
"""Register a mock response for PUT on the CS thread flag endpoints"""
|
||||
self.register_flag_response("thread", thread_id)
|
||||
|
||||
def register_comment_flag_response(self, comment_id):
|
||||
"""Register a mock response for PUT on the CS comment flag endpoints"""
|
||||
self.register_flag_response("comment", comment_id)
|
||||
|
||||
def assert_query_params_equal(self, httpretty_request, expected_params):
|
||||
"""
|
||||
Assert that the given mock request had the expected query parameters
|
||||
"""
|
||||
actual_params = dict(querystring(httpretty_request))
|
||||
actual_params.pop("request_id") # request_id is random
|
||||
assert actual_params == expected_params
|
||||
|
||||
def assert_last_query_params(self, expected_params):
|
||||
"""
|
||||
Assert that the last mock request had the expected query parameters
|
||||
"""
|
||||
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(
|
||||
self.url,
|
||||
json.dumps(request_data),
|
||||
content_type="application/merge-patch+json"
|
||||
)
|
||||
|
||||
def expected_thread_data(self, overrides=None):
|
||||
"""
|
||||
Returns expected thread data in API response
|
||||
"""
|
||||
response_data = {
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"author": self.user.username,
|
||||
"author_label": None,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"updated_at": "1970-01-01T00:00:00Z",
|
||||
"raw_body": "Test body",
|
||||
"rendered_body": "<p>Test body</p>",
|
||||
"preview_body": "Test body",
|
||||
"abuse_flagged": False,
|
||||
"abuse_flagged_count": None,
|
||||
"voted": False,
|
||||
"vote_count": 0,
|
||||
"editable_fields": [
|
||||
"abuse_flagged",
|
||||
"anonymous",
|
||||
"copy_link",
|
||||
"following",
|
||||
"raw_body",
|
||||
"read",
|
||||
"title",
|
||||
"topic_id",
|
||||
"type",
|
||||
],
|
||||
"course_id": str(self.course.id),
|
||||
"topic_id": "test_topic",
|
||||
"group_id": None,
|
||||
"group_name": None,
|
||||
"title": "Test Title",
|
||||
"pinned": False,
|
||||
"closed": False,
|
||||
"can_delete": True,
|
||||
"following": False,
|
||||
"comment_count": 1,
|
||||
"unread_comment_count": 0,
|
||||
"comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
|
||||
"endorsed_comment_list_url": None,
|
||||
"non_endorsed_comment_list_url": None,
|
||||
"read": False,
|
||||
"has_endorsed": False,
|
||||
"id": "test_thread",
|
||||
"type": "discussion",
|
||||
"response_count": 0,
|
||||
"last_edit": None,
|
||||
"edit_by_label": None,
|
||||
"closed_by": None,
|
||||
"closed_by_label": None,
|
||||
"close_reason": None,
|
||||
"close_reason_code": None,
|
||||
}
|
||||
response_data.update(overrides or {})
|
||||
return response_data
|
||||
|
||||
|
||||
def make_minimal_cs_thread(overrides=None):
|
||||
"""
|
||||
Create a dictionary containing all needed thread fields as returned by the
|
||||
|
||||
70
lms/djangoapps/discussion/tests/utils.py
Normal file
70
lms/djangoapps/discussion/tests/utils.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Utils for the discussion app.
|
||||
"""
|
||||
|
||||
|
||||
def make_minimal_cs_thread(overrides=None):
|
||||
"""
|
||||
Create a dictionary containing all needed thread fields as returned by the
|
||||
comments service with dummy data and optional overrides
|
||||
"""
|
||||
ret = {
|
||||
"type": "thread",
|
||||
"id": "dummy",
|
||||
"course_id": "course-v1:dummy+dummy+dummy",
|
||||
"commentable_id": "dummy",
|
||||
"group_id": None,
|
||||
"user_id": "0",
|
||||
"username": "dummy",
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"updated_at": "1970-01-01T00:00:00Z",
|
||||
"last_activity_at": "1970-01-01T00:00:00Z",
|
||||
"thread_type": "discussion",
|
||||
"title": "dummy",
|
||||
"body": "dummy",
|
||||
"pinned": False,
|
||||
"closed": False,
|
||||
"abuse_flaggers": [],
|
||||
"abuse_flagged_count": None,
|
||||
"votes": {"up_count": 0},
|
||||
"comments_count": 0,
|
||||
"unread_comments_count": 0,
|
||||
"children": [],
|
||||
"read": False,
|
||||
"endorsed": False,
|
||||
"resp_total": 0,
|
||||
"closed_by": None,
|
||||
"close_reason_code": None,
|
||||
}
|
||||
ret.update(overrides or {})
|
||||
return ret
|
||||
|
||||
|
||||
def make_minimal_cs_comment(overrides=None):
|
||||
"""
|
||||
Create a dictionary containing all needed comment fields as returned by the
|
||||
comments service with dummy data and optional overrides
|
||||
"""
|
||||
ret = {
|
||||
"type": "comment",
|
||||
"id": "dummy",
|
||||
"commentable_id": "dummy",
|
||||
"thread_id": "dummy",
|
||||
"parent_id": None,
|
||||
"user_id": "0",
|
||||
"username": "dummy",
|
||||
"anonymous": False,
|
||||
"anonymous_to_peers": False,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"updated_at": "1970-01-01T00:00:00Z",
|
||||
"body": "dummy",
|
||||
"abuse_flaggers": [],
|
||||
"votes": {"up_count": 0},
|
||||
"endorsed": False,
|
||||
"child_count": 0,
|
||||
"children": [],
|
||||
}
|
||||
ret.update(overrides or {})
|
||||
return ret
|
||||
@@ -3,10 +3,9 @@ from bs4 import BeautifulSoup
|
||||
|
||||
from openedx.core.djangoapps.django_comment_common.comment_client import models, settings
|
||||
|
||||
from .thread import Thread, _url_for_flag_abuse_thread, _url_for_unflag_abuse_thread
|
||||
from .utils import CommentClientRequestError, get_course_key, perform_request
|
||||
from .thread import Thread
|
||||
from .utils import CommentClientRequestError, get_course_key
|
||||
from forum import api as forum_api
|
||||
from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled
|
||||
|
||||
|
||||
class Comment(models.Model):
|
||||
@@ -64,71 +63,30 @@ class Comment(models.Model):
|
||||
return super().url(action, params)
|
||||
|
||||
def flagAbuse(self, user, voteable, course_id=None):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_flag_abuse_thread(voteable.id)
|
||||
elif voteable.type == 'comment':
|
||||
url = _url_for_flag_abuse_comment(voteable.id)
|
||||
else:
|
||||
raise CommentClientRequestError("Can only flag/unflag threads or comments")
|
||||
if voteable.type != 'comment':
|
||||
raise CommentClientRequestError("Can only flag comments")
|
||||
|
||||
course_key = get_course_key(self.attributes.get("course_id") or course_id)
|
||||
if is_forum_v2_enabled(course_key):
|
||||
if voteable.type == 'thread':
|
||||
response = forum_api.update_thread_flag(
|
||||
voteable.id, "flag", user_id=user.id, course_id=str(course_key)
|
||||
)
|
||||
else:
|
||||
response = forum_api.update_comment_flag(
|
||||
voteable.id, "flag", user_id=user.id, course_id=str(course_key)
|
||||
)
|
||||
else:
|
||||
params = {'user_id': user.id}
|
||||
response = perform_request(
|
||||
'put',
|
||||
url,
|
||||
params,
|
||||
metric_tags=self._metric_tags,
|
||||
metric_action='comment.abuse.flagged'
|
||||
)
|
||||
response = forum_api.update_comment_flag(
|
||||
comment_id=voteable.id,
|
||||
action="flag",
|
||||
user_id=str(user.id),
|
||||
course_id=str(course_key),
|
||||
)
|
||||
voteable._update_from_response(response)
|
||||
|
||||
def unFlagAbuse(self, user, voteable, removeAll, course_id=None):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_unflag_abuse_thread(voteable.id)
|
||||
elif voteable.type == 'comment':
|
||||
url = _url_for_unflag_abuse_comment(voteable.id)
|
||||
else:
|
||||
raise CommentClientRequestError("Can flag/unflag for threads or comments")
|
||||
if voteable.type != 'comment':
|
||||
raise CommentClientRequestError("Can only unflag comments")
|
||||
|
||||
course_key = get_course_key(self.attributes.get("course_id") or course_id)
|
||||
if is_forum_v2_enabled(course_key):
|
||||
if voteable.type == "thread":
|
||||
response = forum_api.update_thread_flag(
|
||||
thread_id=voteable.id,
|
||||
action="unflag",
|
||||
user_id=user.id,
|
||||
update_all=bool(removeAll),
|
||||
course_id=str(course_key)
|
||||
)
|
||||
else:
|
||||
response = forum_api.update_comment_flag(
|
||||
comment_id=voteable.id,
|
||||
action="unflag",
|
||||
user_id=user.id,
|
||||
update_all=bool(removeAll),
|
||||
course_id=str(course_key)
|
||||
)
|
||||
else:
|
||||
params = {'user_id': user.id}
|
||||
|
||||
if removeAll:
|
||||
params['all'] = True
|
||||
|
||||
response = perform_request(
|
||||
'put',
|
||||
url,
|
||||
params,
|
||||
metric_tags=self._metric_tags,
|
||||
metric_action='comment.abuse.unflagged'
|
||||
)
|
||||
response = forum_api.update_comment_flag(
|
||||
comment_id=voteable.id,
|
||||
action="unflag",
|
||||
user_id=str(user.id),
|
||||
update_all=bool(removeAll),
|
||||
course_id=str(course_key),
|
||||
)
|
||||
voteable._update_from_response(response)
|
||||
|
||||
@property
|
||||
|
||||
@@ -170,7 +170,6 @@ class Model:
|
||||
response = self.handle_update(params)
|
||||
else: # otherwise, treat this as an insert
|
||||
response = self.handle_create(params)
|
||||
|
||||
self.retrieved = True
|
||||
self._update_from_response(response)
|
||||
self.after_save(self)
|
||||
@@ -256,7 +255,7 @@ class Model:
|
||||
request_data = {
|
||||
"comment_id": self.attributes["id"],
|
||||
"body": request_params.get("body"),
|
||||
"course_id": request_params.get("course_id"),
|
||||
"course_id": request_params.get("course_id") or course_id,
|
||||
"user_id": request_params.get("user_id"),
|
||||
"anonymous": request_params.get("anonymous"),
|
||||
"anonymous_to_peers": request_params.get("anonymous_to_peers"),
|
||||
@@ -265,7 +264,6 @@ class Model:
|
||||
"editing_user_id": request_params.get("editing_user_id"),
|
||||
"edit_reason_code": request_params.get("edit_reason_code"),
|
||||
"endorsement_user_id": request_params.get("endorsement_user_id"),
|
||||
"course_key": course_id
|
||||
}
|
||||
request_data = {k: v for k, v in request_data.items() if v is not None}
|
||||
response = forum_api.update_comment(**request_data)
|
||||
@@ -276,7 +274,7 @@ class Model:
|
||||
"thread_id": self.attributes["id"],
|
||||
"title": request_params.get("title"),
|
||||
"body": request_params.get("body"),
|
||||
"course_id": request_params.get("course_id"),
|
||||
"course_id": request_params.get("course_id") or course_id,
|
||||
"anonymous": request_params.get("anonymous"),
|
||||
"anonymous_to_peers": request_params.get("anonymous_to_peers"),
|
||||
"closed": request_params.get("closed"),
|
||||
@@ -289,7 +287,7 @@ class Model:
|
||||
"close_reason_code": request_params.get("close_reason_code"),
|
||||
"closing_user_id": request_params.get("closing_user_id"),
|
||||
"endorsed": request_params.get("endorsed"),
|
||||
"course_key": course_id
|
||||
"read": request_params.get("read"),
|
||||
}
|
||||
request_data = {k: v for k, v in request_data.items() if v is not None}
|
||||
response = forum_api.update_thread(**request_data)
|
||||
|
||||
@@ -199,51 +199,31 @@ class Thread(models.Model):
|
||||
self._update_from_response(response)
|
||||
|
||||
def flagAbuse(self, user, voteable, course_id=None):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_flag_abuse_thread(voteable.id)
|
||||
else:
|
||||
raise utils.CommentClientRequestError("Can only flag/unflag threads or comments")
|
||||
if voteable.type != 'thread':
|
||||
raise utils.CommentClientRequestError("Can only flag threads")
|
||||
|
||||
course_key = utils.get_course_key(self.attributes.get("course_id") or course_id)
|
||||
if is_forum_v2_enabled(course_key):
|
||||
response = forum_api.update_thread_flag(voteable.id, "flag", user_id=user.id, course_id=str(course_key))
|
||||
else:
|
||||
params = {'user_id': user.id}
|
||||
response = utils.perform_request(
|
||||
'put',
|
||||
url,
|
||||
params,
|
||||
metric_action='thread.abuse.flagged',
|
||||
metric_tags=self._metric_tags
|
||||
)
|
||||
response = forum_api.update_thread_flag(
|
||||
thread_id=voteable.id,
|
||||
action="flag",
|
||||
user_id=str(user.id),
|
||||
course_id=str(course_key)
|
||||
)
|
||||
voteable._update_from_response(response)
|
||||
|
||||
def unFlagAbuse(self, user, voteable, removeAll, course_id=None):
|
||||
if voteable.type == 'thread':
|
||||
url = _url_for_unflag_abuse_thread(voteable.id)
|
||||
else:
|
||||
raise utils.CommentClientRequestError("Can only flag/unflag for threads or comments")
|
||||
course_key = utils.get_course_key(self.attributes.get("course_id") or course_id)
|
||||
if is_forum_v2_enabled(course_key):
|
||||
response = forum_api.update_thread_flag(
|
||||
thread_id=voteable.id,
|
||||
action="unflag",
|
||||
user_id=user.id,
|
||||
update_all=bool(removeAll),
|
||||
course_id=str(course_key)
|
||||
)
|
||||
else:
|
||||
params = {'user_id': user.id}
|
||||
#if you're an admin, when you unflag, remove ALL flags
|
||||
if removeAll:
|
||||
params['all'] = True
|
||||
if voteable.type != 'thread':
|
||||
raise utils.CommentClientRequestError("Can only unflag threads")
|
||||
|
||||
course_key = utils.get_course_key(self.attributes.get("course_id") or course_id)
|
||||
response = forum_api.update_thread_flag(
|
||||
thread_id=voteable.id,
|
||||
action="unflag",
|
||||
user_id=user.id,
|
||||
update_all=bool(removeAll),
|
||||
course_id=str(course_key)
|
||||
)
|
||||
|
||||
response = utils.perform_request(
|
||||
'put',
|
||||
url,
|
||||
params,
|
||||
metric_tags=self._metric_tags,
|
||||
metric_action='thread.abuse.unflagged'
|
||||
)
|
||||
voteable._update_from_response(response)
|
||||
|
||||
def pin(self, user, thread_id, course_id=None):
|
||||
|
||||
Reference in New Issue
Block a user