diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests.py b/lms/djangoapps/discussion/django_comment_client/base/tests.py index bc2253b140..dce787ac11 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/tests.py +++ b/lms/djangoapps/discussion/django_comment_client/base/tests.py @@ -527,23 +527,6 @@ class ViewsTestCase( # create_thread_helper verifies that extra data are passed through to the comments service self.create_thread_helper(mock_is_forum_v2_enabled, mock_request, extra_response_data={'context': ThreadContext.STANDALONE}) - @ddt.data( - ('follow_thread', 'thread_followed'), - ('unfollow_thread', 'thread_unfollowed'), - ) - @ddt.unpack - def test_follow_unfollow_thread_signals(self, view_name, signal, mock_is_forum_v2_enabled, mock_request): - self.create_thread_helper(mock_is_forum_v2_enabled, mock_request) - - with self.assert_discussion_signals(signal): - response = self.client.post( - reverse( - view_name, - kwargs={"course_id": str(self.course_id), "thread_id": 'i4x-MITx-999-course-Robot_Super_Course'} - ) - ) - assert response.status_code == 200 - def test_delete_thread(self, mock_is_forum_v2_enabled, mock_request): mock_is_forum_v2_enabled.return_value = False self._set_mock_request_data(mock_request, { @@ -1421,27 +1404,6 @@ class TeamsPermissionsTestCase(ForumsEnableMixin, UrlResetMixin, SharedModuleSto ) assert response.status_code == status_code - @ddt.data(*ddt_permissions_args) - @ddt.unpack - def test_threads_actions(self, user, commentable_id, status_code, mock_is_forum_v2_enabled, mock_request): - """ - 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, 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 ["follow_thread", "unfollow_thread"]: - response = self.client.post( - reverse( - action, - kwargs={"course_id": str(self.course.id), "thread_id": "dummy_thread"} - ) - ) - assert response.status_code == status_code - TEAM_COMMENTABLE_ID = 'test-team-discussion' @@ -1482,50 +1444,6 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque cls.student.roles.add(Role.objects.get(name="Student", course_id=cls.course.id)) CourseAccessRoleFactory(course_id=cls.course.id, user=cls.student, role='Wizard') - @patch('eventtracking.tracker.emit') - @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) - def test_response_event(self, mock_is_forum_v2_enabled, mock_request, mock_emit): - """ - Check to make sure an event is fired when a user responds to a thread. - """ - event_receiver = Mock() - FORUM_THREAD_RESPONSE_CREATED.connect(event_receiver) - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, { - "closed": False, - "commentable_id": 'test_commentable_id', - 'thread_id': 'test_thread_id', - }) - request = RequestFactory().post("dummy_url", {"body": "Test comment", 'auto_subscribe': True}) - request.user = self.student - request.view_name = "create_comment" - views.create_comment(request, course_id=str(self.course.id), thread_id='test_thread_id') - - event_name, event = mock_emit.call_args[0] - assert event_name == 'edx.forum.response.created' - assert event['body'] == 'Test comment' - assert event['commentable_id'] == 'test_commentable_id' - assert event['user_forums_roles'] == ['Student'] - assert event['user_course_roles'] == ['Wizard'] - assert event['discussion']['id'] == 'test_thread_id' - assert event['options']['followed'] is True - - event_receiver.assert_called_once() - - self.assertDictContainsSubset( - { - "signal": FORUM_THREAD_RESPONSE_CREATED, - "sender": None, - }, - event_receiver.call_args.kwargs - ) - - self.assertIn( - "thread", - event_receiver.call_args.kwargs - ) - @patch('eventtracking.tracker.emit') @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) @@ -1570,108 +1488,6 @@ class ForumEventTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockReque event_receiver.call_args.kwargs ) - @patch('eventtracking.tracker.emit') - @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) - @ddt.data(( - 'create_thread', - 'edx.forum.thread.created', { - 'thread_type': 'discussion', - 'body': 'Test text', - 'title': 'Test', - 'auto_subscribe': True - }, - {'commentable_id': TEAM_COMMENTABLE_ID} - ), ( - 'create_comment', - 'edx.forum.response.created', - {'body': 'Test comment', 'auto_subscribe': True}, - {'thread_id': 'test_thread_id'} - ), ( - 'create_sub_comment', - 'edx.forum.comment.created', - {'body': 'Another comment'}, - {'comment_id': 'dummy_comment_id'} - )) - @ddt.unpack - def test_team_events(self, view_name, event_name, view_data, view_kwargs, mock_is_forum_v2_enabled, mock_request, mock_emit): - user = self.student - team = CourseTeamFactory.create(discussion_topic_id=TEAM_COMMENTABLE_ID) - CourseTeamMembershipFactory.create(team=team, user=user) - - event_receiver = Mock() - forum_event = views.TRACKING_LOG_TO_EVENT_MAPS.get(event_name) - forum_event.connect(event_receiver) - - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, { - 'closed': False, - 'commentable_id': TEAM_COMMENTABLE_ID, - 'thread_id': 'test_thread_id', - }) - - request = RequestFactory().post('dummy_url', view_data) - request.user = user - request.view_name = view_name - - getattr(views, view_name)(request, course_id=str(self.course.id), **view_kwargs) - - name, event = mock_emit.call_args[0] - assert name == event_name - assert event['team_id'] == team.team_id - - self.assertDictContainsSubset( - { - "signal": forum_event, - "sender": None, - }, - event_receiver.call_args.kwargs - ) - - self.assertIn( - "thread", - event_receiver.call_args.kwargs - ) - - @ddt.data('follow_thread', 'unfollow_thread',) - @patch('eventtracking.tracker.emit') - @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) - def test_thread_followed_event(self, view_name, mock_is_forum_v2_enabled, mock_request, mock_emit): - event_receiver = Mock() - for signal in views.TRACKING_LOG_TO_EVENT_MAPS.values(): - signal.connect(event_receiver) - - mock_is_forum_v2_enabled.return_value = False - self._set_mock_request_data(mock_request, { - 'closed': False, - 'commentable_id': 'test_commentable_id', - 'username': 'test_user', - }) - request = RequestFactory().post('dummy_url', {}) - request.user = self.student - request.view_name = view_name - view_function = getattr(views, view_name) - kwargs = dict(course_id=str(self.course.id)) - kwargs['thread_id'] = 'thread_id' - view_function(request, **kwargs) - - assert mock_emit.called - event_name, event_data = mock_emit.call_args[0] - action_name = 'followed' if view_name == 'follow_thread' else 'unfollowed' - expected_action_value = True if view_name == 'follow_thread' else False - assert event_name == f'edx.forum.thread.{action_name}' - assert event_data['commentable_id'] == 'test_commentable_id' - assert event_data['id'] == 'thread_id' - assert event_data['followed'] == expected_action_value - assert event_data['user_forums_roles'] == ['Student'] - assert event_data['user_course_roles'] == ['Wizard'] - - # In case of events that doesn't have a correspondig Open edX events signal - # we need to check that none of the openedx signals is called. - # This is tested for all the events that are not tested above. - event_receiver.assert_not_called() - class UsersEndpointTestCase(ForumsEnableMixin, SharedModuleStoreTestCase, MockRequestSetupMixin): diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py b/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py index 0d1faa55d7..bef31367d5 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py +++ b/lms/djangoapps/discussion/django_comment_client/base/tests_v2.py @@ -593,6 +593,22 @@ class ViewsTestCase( ) assert response.status_code == 200 + @ddt.data( + ('follow_thread', 'thread_followed'), + ('unfollow_thread', 'thread_unfollowed'), + ) + @ddt.unpack + def test_follow_unfollow_thread_signals(self, view_name, signal): + self._setup_mock_request("get_thread") + with self.assert_discussion_signals(signal): + response = self.client.post( + reverse( + view_name, + kwargs={"course_id": str(self.course_id), "thread_id": "i4x-MITx-999-course-Robot_Super_Course"} + ) + ) + assert response.status_code == 200 + @disable_signal(views, "comment_endorsed") class ViewPermissionsTestCase( @@ -956,6 +972,9 @@ class TeamsPermissionsTestCase( assert response.status_code == status_code +TEAM_COMMENTABLE_ID = 'test-team-discussion' + + @disable_signal(views, "comment_created") @ddt.ddt class ForumEventTestCase( @@ -1038,3 +1057,165 @@ class ForumEventTestCase( assert event["target_username"] == "gumprecht" assert event["undo_vote"] == undo assert event["vote_value"] == "up" + + @patch('eventtracking.tracker.emit') + @ddt.data(( + 'create_thread', + 'edx.forum.thread.created', { + 'thread_type': 'discussion', + 'body': 'Test text', + 'title': 'Test', + 'auto_subscribe': True + }, + {'commentable_id': TEAM_COMMENTABLE_ID} + ), ( + 'create_comment', + 'edx.forum.response.created', + {'body': 'Test comment', 'auto_subscribe': True}, + {'thread_id': 'test_thread_id'} + ), ( + 'create_sub_comment', + 'edx.forum.comment.created', + {'body': 'Another comment'}, + {'comment_id': 'dummy_comment_id'} + )) + @ddt.unpack + def test_team_events(self, view_name, event_name, view_data, view_kwargs, mock_emit): + user = self.student + team = CourseTeamFactory.create(discussion_topic_id=TEAM_COMMENTABLE_ID) + CourseTeamMembershipFactory.create(team=team, user=user) + cs_thread = make_minimal_cs_thread( + { + "commentable_id": "test_commentable_id", + "username": "gumprecht", + } + ) + cs_comment = make_minimal_cs_comment( + { + "closed": False, + "commentable_id": "test_commentable_id", + "username": "gumprecht", + } + ) + mock_request_data = { + 'closed': False, + 'commentable_id': TEAM_COMMENTABLE_ID, + 'thread_id': 'test_thread_id', + } + self.set_mock_return_value("create_thread", mock_request_data) + self.set_mock_return_value("get_thread", mock_request_data) + self.set_mock_return_value("create_comment", mock_request_data) + self.set_mock_return_value("create_parent_comment", mock_request_data) + self.set_mock_return_value("get_parent_comment", mock_request_data) + self.set_mock_return_value("create_child_comment", mock_request_data) + self.set_mock_return_value("create_sub_comment", mock_request_data) + + event_receiver = Mock() + forum_event = views.TRACKING_LOG_TO_EVENT_MAPS.get(event_name) + forum_event.connect(event_receiver) + + request = RequestFactory().post('dummy_url', view_data) + request.user = user + request.view_name = view_name + + getattr(views, view_name)(request, course_id=str(self.course.id), **view_kwargs) + + name, event = mock_emit.call_args[0] + assert name == event_name + assert event['team_id'] == team.team_id + + self.assertDictContainsSubset( + { + "signal": forum_event, + "sender": None, + }, + event_receiver.call_args.kwargs + ) + + self.assertIn( + "thread", + event_receiver.call_args.kwargs + ) + + @ddt.data('follow_thread', 'unfollow_thread',) + @patch('eventtracking.tracker.emit') + def test_thread_followed_event(self, view_name, mock_emit): + event_receiver = Mock() + for signal in views.TRACKING_LOG_TO_EVENT_MAPS.values(): + signal.connect(event_receiver) + + mock_request_data = { + 'closed': False, + 'commentable_id': 'test_commentable_id', + 'username': 'test_user', + } + self.set_mock_return_value("get_thread", mock_request_data) + self.set_mock_return_value("follow_thread", mock_request_data) + self.set_mock_return_value("unfollow_thread", mock_request_data) + request = RequestFactory().post('dummy_url', {}) + request.user = self.student + request.view_name = view_name + view_function = getattr(views, view_name) + kwargs = dict(course_id=str(self.course.id)) + kwargs['thread_id'] = 'thread_id' + view_function(request, **kwargs) + + assert mock_emit.called + event_name, event_data = mock_emit.call_args[0] + action_name = 'followed' if view_name == 'follow_thread' else 'unfollowed' + expected_action_value = True if view_name == 'follow_thread' else False + assert event_name == f'edx.forum.thread.{action_name}' + assert event_data['commentable_id'] == 'test_commentable_id' + assert event_data['id'] == 'thread_id' + assert event_data['followed'] == expected_action_value + assert event_data['user_forums_roles'] == ['Student'] + assert event_data['user_course_roles'] == ['Wizard'] + + # In case of events that doesn't have a correspondig Open edX events signal + # we need to check that none of the openedx signals is called. + # This is tested for all the events that are not tested above. + event_receiver.assert_not_called() + + @patch('eventtracking.tracker.emit') + def test_response_event(self, mock_emit): + """ + Check to make sure an event is fired when a user responds to a thread. + """ + event_receiver = Mock() + FORUM_THREAD_RESPONSE_CREATED.connect(event_receiver) + mock_request_data = { + "closed": False, + "commentable_id": 'test_commentable_id', + 'thread_id': 'test_thread_id', + } + self.set_mock_return_value("get_thread", mock_request_data) + self.set_mock_return_value("create_parent_comment", mock_request_data) + + request = RequestFactory().post("dummy_url", {"body": "Test comment", 'auto_subscribe': True}) + request.user = self.student + request.view_name = "create_comment" + views.create_comment(request, course_id=str(self.course.id), thread_id='test_thread_id') + + event_name, event = mock_emit.call_args[0] + assert event_name == 'edx.forum.response.created' + assert event['body'] == 'Test comment' + assert event['commentable_id'] == 'test_commentable_id' + assert event['user_forums_roles'] == ['Student'] + assert event['user_course_roles'] == ['Wizard'] + assert event['discussion']['id'] == 'test_thread_id' + assert event['options']['followed'] is True + + event_receiver.assert_called_once() + + self.assertDictContainsSubset( + { + "signal": FORUM_THREAD_RESPONSE_CREATED, + "sender": None, + }, + event_receiver.call_args.kwargs + ) + + self.assertIn( + "thread", + event_receiver.call_args.kwargs + ) diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api.py b/lms/djangoapps/discussion/rest_api/tests/test_api.py index 3a766b55c5..ab565ee846 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api.py @@ -7,7 +7,7 @@ import itertools import random from datetime import datetime, timedelta from unittest import mock -from urllib.parse import parse_qs, urlencode, urlparse, urlunparse +from urllib.parse import urlencode, urlparse, urlunparse import ddt import httpretty @@ -1625,18 +1625,6 @@ class CreateThreadTest( if not expected_error: self.fail(f"Unexpected validation error: {ex}") - def test_following(self): - self.register_post_thread_response({"id": "test_id", "username": self.user.username}) - self.register_subscription_response(self.user) - data = self.minimal_data.copy() - data["following"] = "True" - result = create_thread(self.request, data) - assert result['following'] is True - cs_request = httpretty.last_request() - assert urlparse(cs_request.path).path == f"/api/v1/users/{self.user.id}/subscriptions" # lint-amnesty, pylint: disable=no-member - assert cs_request.method == 'POST' - assert parsed_body(cs_request) == {'source_type': ['thread'], 'source_id': ['test_id']} - def test_course_id_missing(self): with pytest.raises(ValidationError) as assertion: create_thread(self.request, {}) @@ -2251,52 +2239,6 @@ class UpdateThreadTest( assert expected_error assert err.message_dict == {field: ['This field is not editable.'] for field in data.keys()} - @ddt.data(*itertools.product([True, False], [True, False])) - @ddt.unpack - @mock.patch("eventtracking.tracker.emit") - def test_following(self, old_following, new_following, mock_emit): - """ - Test attempts to edit the "following" field. - - old_following indicates whether the thread should be followed at the - start of the test. new_following indicates the value for the "following" - field in the update. If old_following and new_following are the same, no - update should be made. Otherwise, a subscription should be POSTed or - DELETEd according to the new_following value. - """ - if old_following: - self.register_get_user_response(self.user, subscribed_thread_ids=["test_thread"]) - self.register_subscription_response(self.user) - self.register_thread() - data = {"following": new_following} - signal_name = "thread_followed" if new_following else "thread_unfollowed" - mock_path = f"openedx.core.djangoapps.django_comment_common.signals.{signal_name}.send" - with mock.patch(mock_path) as signal_patch: - result = update_thread(self.request, "test_thread", data) - if old_following != new_following: - self.assertEqual(signal_patch.call_count, 1) - assert result['following'] == new_following - last_request_path = urlparse(httpretty.last_request().path).path # lint-amnesty, pylint: disable=no-member - subscription_url = f"/api/v1/users/{self.user.id}/subscriptions" - if old_following == new_following: - assert last_request_path != subscription_url - else: - assert last_request_path == subscription_url - assert httpretty.last_request().method == ('POST' if new_following else 'DELETE') - request_data = ( - parsed_body(httpretty.last_request()) if new_following else - parse_qs(urlparse(httpretty.last_request().path).query) # lint-amnesty, pylint: disable=no-member - ) - request_data.pop("request_id", None) - assert request_data == {'source_type': ['thread'], 'source_id': ['test_thread']} - event_name, event_data = mock_emit.call_args[0] - expected_event_action = 'followed' if new_following else 'unfollowed' - assert event_name == f'edx.forum.thread.{expected_event_action}' - assert event_data['commentable_id'] == 'original_topic' - assert event_data['id'] == 'test_thread' - assert event_data['followed'] == new_following - assert event_data['user_forums_roles'] == ['Student'] - def test_invalid_field(self): self.register_thread() with pytest.raises(ValidationError) as assertion: diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py b/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py index 4efadd6385..62284b904c 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api_v2.py @@ -282,6 +282,18 @@ class CreateThreadTest( } self.check_mock_called_with("update_thread_flag", -1, **params) + def test_following(self): + self.register_post_thread_response({"id": "test_id", "username": self.user.username}) + self.register_subscription_response(self.user) + data = self.minimal_data.copy() + data["following"] = "True" + result = create_thread(self.request, data) + assert result['following'] is True + self.check_mock_called("create_subscription") + + params = {'user_id': str(self.user.id), 'course_id': str(self.course.id), 'source_id': 'test_id'} + self.check_mock_called_with("create_subscription", 0, **params) + @ddt.ddt @disable_signal(api, "comment_created") @@ -712,6 +724,49 @@ 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_following(self, old_following, new_following, mock_emit): + """ + Test attempts to edit the "following" field. + + old_following indicates whether the thread should be followed at the + start of the test. new_following indicates the value for the "following" + field in the update. If old_following and new_following are the same, no + update should be made. Otherwise, a subscription should be POSTed or + DELETEd according to the new_following value. + """ + if old_following: + self.register_get_user_response(self.user, subscribed_thread_ids=["test_thread"]) + self.register_subscription_response(self.user) + self.register_thread() + data = {"following": new_following} + signal_name = "thread_followed" if new_following else "thread_unfollowed" + mock_path = f"openedx.core.djangoapps.django_comment_common.signals.{signal_name}.send" + with mock.patch(mock_path) as signal_patch: + result = update_thread(self.request, "test_thread", data) + if old_following != new_following: + self.assertEqual(signal_patch.call_count, 1) + assert result['following'] == new_following + + if old_following == new_following: + assert not self.check_mock_called("create_subscription") + else: + params = {'user_id': str(self.user.id), 'course_id': str(self.course.id), 'source_id': 'test_thread'} + if new_following: + assert self.check_mock_called("create_subscription") + else: + assert self.check_mock_called("delete_subscription") + + event_name, event_data = mock_emit.call_args[0] + expected_event_action = 'followed' if new_following else 'unfollowed' + assert event_name == f'edx.forum.thread.{expected_event_action}' + assert event_data['commentable_id'] == 'original_topic' + assert event_data['id'] == 'test_thread' + assert event_data['followed'] == new_following + assert event_data['user_forums_roles'] == ['Student'] + @ddt.ddt @disable_signal(api, "comment_edited") diff --git a/lms/djangoapps/discussion/rest_api/tests/test_tasks.py b/lms/djangoapps/discussion/rest_api/tests/test_tasks.py index 70aeb8dd4a..5fd03df0c7 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_tasks.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_tasks.py @@ -2,7 +2,6 @@ Test cases for tasks.py """ from unittest import mock -from unittest.mock import Mock import ddt import httpretty @@ -15,7 +14,6 @@ from common.djangoapps.student.tests.factories import StaffFactory, UserFactory from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory from lms.djangoapps.discussion.rest_api.tasks import ( send_response_endorsed_notifications, - send_response_notifications, send_thread_created_notification ) from lms.djangoapps.discussion.rest_api.tests.utils import ThreadMock, make_minimal_cs_thread @@ -33,7 +31,6 @@ from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICAT from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory -from ..discussions_notifications import DiscussionNotificationSender from .test_views import DiscussionAPIViewTestMixin @@ -273,420 +270,6 @@ class TestNewThreadCreatedNotification(DiscussionAPIViewTestMixin, ModuleStoreTe self.assertEqual(handler.call_count, 1) -@ddt.ddt -@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) -class TestSendResponseNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestCase): - """ - Test for the send_response_notifications function - """ - - def setUp(self): - super().setUp() - httpretty.reset() - httpretty.enable() - patcher = mock.patch( - 'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', - return_value=False - ) - patcher.start() - self.addCleanup(patcher.stop) - - self.course = CourseFactory.create() - patcher = mock.patch( - "openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread", - return_value=self.course.id - ) - self.mock_get_course_id_by_thread = 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", - return_value=self.course.id - ) - self.mock_get_course_id_by_comment = patcher.start() - self.addCleanup(patcher.stop) - self.user_1 = UserFactory.create() - CourseEnrollment.enroll(self.user_1, self.course.id) - self.user_2 = UserFactory.create() - CourseEnrollment.enroll(self.user_2, self.course.id) - self.user_3 = UserFactory.create() - CourseEnrollment.enroll(self.user_3, self.course.id) - self.thread = ThreadMock(thread_id=1, creator=self.user_1, title='test thread') - self.thread_2 = ThreadMock(thread_id=2, creator=self.user_2, title='test thread 2') - self.thread_3 = ThreadMock(thread_id=2, creator=self.user_1, title='test thread 3') - for thread in [self.thread, self.thread_2, self.thread_3]: - self.register_get_thread_response({ - 'id': thread.id, - 'course_id': str(self.course.id), - 'topic_id': 'abc', - "user_id": thread.user_id, - "username": thread.username, - "thread_type": 'discussion', - "title": thread.title, - "commentable_id": thread.commentable_id, - - }) - self._register_subscriptions_endpoint() - - self.comment = ThreadMock(thread_id=4, creator=self.user_2, title='test comment', body='comment body') - self.register_get_comment_response( - { - 'id': self.comment.id, - 'thread_id': self.thread.id, - 'parent_id': None, - 'user_id': self.comment.user_id, - 'body': self.comment.body, - } - ) - - def test_basic(self): - """ - Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin - """ - - def test_not_authenticated(self): - """ - Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin - """ - - def test_send_notification_to_thread_creator(self): - """ - Test that the notification is sent to the thread creator - """ - handler = mock.Mock() - USER_NOTIFICATION_REQUESTED.connect(handler) - - # Post the form or do what it takes to send the signal - - send_response_notifications( - self.thread.id, - str(self.course.id), - self.user_2.id, - self.comment.id, - parent_id=None - ) - self.assertEqual(handler.call_count, 2) - args = handler.call_args_list[0][1]['notification_data'] - self.assertEqual([int(user_id) for user_id in args.user_ids], [self.user_1.id]) - self.assertEqual(args.notification_type, 'new_response') - expected_context = { - 'replier_name': self.user_2.username, - 'post_title': 'test thread', - 'email_content': self.comment.body, - 'course_name': self.course.display_name, - 'sender_id': self.user_2.id, - 'response_id': 4, - 'topic_id': None, - 'thread_id': 1, - 'comment_id': None, - } - self.assertDictEqual(args.context, expected_context) - self.assertEqual( - args.content_url, - _get_mfe_url(self.course.id, self.thread.id) - ) - self.assertEqual(args.app_name, 'discussion') - - def test_send_notification_to_parent_threads(self): - """ - Test that the notification signal is sent to the parent response creator and - parent thread creator, it checks signal is sent with correct arguments for both - types of notifications. - """ - handler = mock.Mock() - USER_NOTIFICATION_REQUESTED.connect(handler) - - self.register_get_comment_response({ - 'id': self.thread_2.id, - 'thread_id': self.thread.id, - 'user_id': self.thread_2.user_id - }) - - send_response_notifications( - self.thread.id, - str(self.course.id), - self.user_3.id, - self.comment.id, - parent_id=self.thread_2.id - ) - # check if 2 call are made to the handler i.e. one for the response creator and one for the thread creator - self.assertEqual(handler.call_count, 2) - - # check if the notification is sent to the thread creator - args_comment = handler.call_args_list[0][1]['notification_data'] - args_comment_on_response = handler.call_args_list[1][1]['notification_data'] - self.assertEqual([int(user_id) for user_id in args_comment.user_ids], [self.user_1.id]) - self.assertEqual(args_comment.notification_type, 'new_comment') - expected_context = { - 'replier_name': self.user_3.username, - 'post_title': self.thread.title, - 'email_content': self.comment.body, - 'author_name': 'dummy\'s', - 'author_pronoun': 'dummy\'s', - 'course_name': self.course.display_name, - 'sender_id': self.user_3.id, - 'response_id': 2, - 'topic_id': None, - 'thread_id': 1, - 'comment_id': 4, - } - self.assertDictEqual(args_comment.context, expected_context) - self.assertEqual( - args_comment.content_url, - _get_mfe_url(self.course.id, self.thread.id) - ) - self.assertEqual(args_comment.app_name, 'discussion') - - # check if the notification is sent to the parent response creator - self.assertEqual([int(user_id) for user_id in args_comment_on_response.user_ids], [self.user_2.id]) - self.assertEqual(args_comment_on_response.notification_type, 'new_comment_on_response') - expected_context = { - 'replier_name': self.user_3.username, - 'post_title': self.thread.title, - 'email_content': self.comment.body, - 'course_name': self.course.display_name, - 'sender_id': self.user_3.id, - 'response_id': 2, - 'topic_id': None, - 'thread_id': 1, - 'comment_id': 4, - } - self.assertDictEqual(args_comment_on_response.context, expected_context) - self.assertEqual( - args_comment_on_response.content_url, - _get_mfe_url(self.course.id, self.thread.id) - ) - self.assertEqual(args_comment_on_response.app_name, 'discussion') - - def test_no_signal_on_creators_own_thread(self): - """ - Makes sure that 1 signal is emitted if user creates response on - their own thread. - """ - handler = mock.Mock() - USER_NOTIFICATION_REQUESTED.connect(handler) - - send_response_notifications( - self.thread.id, - str(self.course.id), - self.user_1.id, - self.comment.id, parent_id=None - ) - self.assertEqual(handler.call_count, 1) - - def test_comment_creators_own_response(self): - """ - Check incase post author and response auther is same only send - new comment signal , with your as author_name. - """ - handler = mock.Mock() - USER_NOTIFICATION_REQUESTED.connect(handler) - - self.register_get_comment_response({ - 'id': self.thread_3.id, - 'thread_id': self.thread.id, - 'user_id': self.thread_3.user_id - }) - - send_response_notifications( - self.thread.id, - str(self.course.id), - self.user_3.id, - parent_id=self.thread_2.id, - comment_id=self.comment.id - ) - # check if 1 call is made to the handler i.e. for the thread creator - self.assertEqual(handler.call_count, 2) - - # check if the notification is sent to the thread creator - args_comment = handler.call_args_list[0][1]['notification_data'] - self.assertEqual(args_comment.user_ids, [self.user_1.id]) - self.assertEqual(args_comment.notification_type, 'new_comment') - expected_context = { - 'replier_name': self.user_3.username, - 'post_title': self.thread.title, - 'author_name': 'dummy\'s', - 'author_pronoun': 'your', - 'course_name': self.course.display_name, - 'sender_id': self.user_3.id, - 'email_content': self.comment.body, - 'response_id': 2, - 'topic_id': None, - 'thread_id': 1, - 'comment_id': 4, - } - self.assertDictEqual(args_comment.context, expected_context) - self.assertEqual( - args_comment.content_url, - _get_mfe_url(self.course.id, self.thread.id) - ) - self.assertEqual(args_comment.app_name, 'discussion') - - @ddt.data( - (None, 'response_on_followed_post'), (1, 'comment_on_followed_post') - ) - @ddt.unpack - def test_send_notification_to_followers(self, parent_id, notification_type): - """ - Test that the notification is sent to the followers of the thread - """ - self.register_get_comment_response({ - 'id': self.thread.id, - 'thread_id': self.thread.id, - 'user_id': self.thread.user_id - }) - handler = Mock() - USER_NOTIFICATION_REQUESTED.connect(handler) - - # Post the form or do what it takes to send the signal - notification_sender = DiscussionNotificationSender( - self.thread, - self.course, - self.user_2, - parent_id=parent_id, - comment_id=self.comment.id - ) - notification_sender.send_response_on_followed_post_notification() - self.assertEqual(handler.call_count, 1) - args = handler.call_args[1]['notification_data'] - # only sent to user_3 because user_2 is the one who created the response - self.assertEqual([self.user_3.id], args.user_ids) - self.assertEqual(args.notification_type, notification_type) - expected_context = { - 'replier_name': self.user_2.username, - 'post_title': 'test thread', - 'email_content': self.comment.body, - 'course_name': self.course.display_name, - 'sender_id': self.user_2.id, - 'response_id': 4 if notification_type == 'response_on_followed_post' else parent_id, - 'topic_id': None, - 'thread_id': 1, - 'comment_id': 4 if not notification_type == 'response_on_followed_post' else None, - } - if parent_id: - expected_context['author_name'] = 'dummy\'s' - expected_context['author_pronoun'] = 'dummy\'s' - self.assertDictEqual(args.context, expected_context) - self.assertEqual( - args.content_url, - _get_mfe_url(self.course.id, self.thread.id) - ) - self.assertEqual(args.app_name, 'discussion') - - def _register_subscriptions_endpoint(self): - """ - Registers the endpoint for the subscriptions API - """ - mock_response = { - 'collection': [ - { - '_id': 1, - 'subscriber_id': str(self.user_2.id), - "source_id": self.thread.id, - "source_type": "thread", - }, - { - '_id': 2, - 'subscriber_id': str(self.user_3.id), - "source_id": self.thread.id, - "source_type": "thread", - }, - ], - 'page': 1, - 'num_pages': 1, - 'subscriptions_count': 2, - 'corrected_text': None - - } - self.register_get_subscriptions(self.thread.id, mock_response) - - -@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) -class TestSendCommentNotification(DiscussionAPIViewTestMixin, ModuleStoreTestCase): - """ - Test case to send new_comment notification - """ - - def setUp(self): - super().setUp() - httpretty.reset() - httpretty.enable() - patcher = mock.patch( - 'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', - return_value=False - ) - patcher.start() - self.addCleanup(patcher.stop) - - self.course = CourseFactory.create() - patcher = mock.patch( - "openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread", - return_value=self.course.id - ) - self.mock_get_course_id_by_thread = 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", - return_value=self.course.id - ) - self.mock_get_course_id_by_comment = patcher.start() - self.addCleanup(patcher.stop) - self.user_1 = UserFactory.create() - CourseEnrollment.enroll(self.user_1, self.course.id) - self.user_2 = UserFactory.create() - CourseEnrollment.enroll(self.user_2, self.course.id) - - def test_basic(self): - """ - Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin - """ - - def test_not_authenticated(self): - """ - Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin - """ - - def test_new_comment_notification(self): - """ - Tests new comment notification generation - """ - handler = mock.Mock() - USER_NOTIFICATION_REQUESTED.connect(handler) - - thread = ThreadMock(thread_id=1, creator=self.user_1, title='test thread') - response = ThreadMock(thread_id=2, creator=self.user_2, title='test response') - comment = ThreadMock(thread_id=3, creator=self.user_2, title='test comment', body='comment body') - self.register_get_thread_response({ - 'id': thread.id, - 'course_id': str(self.course.id), - 'topic_id': 'abc', - "user_id": thread.user_id, - "username": thread.username, - "thread_type": 'discussion', - "title": thread.title, - "commentable_id": thread.commentable_id, - - }) - self.register_get_comment_response({ - 'id': response.id, - 'thread_id': thread.id, - 'user_id': response.user_id - }) - self.register_get_comment_response({ - 'id': comment.id, - 'parent_id': response.id, - 'user_id': comment.user_id, - 'body': comment.body - }) - self.register_get_subscriptions(1, {}) - send_response_notifications(thread.id, str(self.course.id), self.user_2.id, parent_id=response.id, - comment_id=comment.id) - handler.assert_called_once() - context = handler.call_args[1]['notification_data'].context - self.assertEqual(context['author_name'], 'dummy\'s') - self.assertEqual(context['author_pronoun'], 'their') - self.assertEqual(context['email_content'], comment.body) - - @override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) class TestResponseEndorsedNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestCase): """ diff --git a/lms/djangoapps/discussion/rest_api/tests/test_tasks_v2.py b/lms/djangoapps/discussion/rest_api/tests/test_tasks_v2.py new file mode 100644 index 0000000000..b6193de2df --- /dev/null +++ b/lms/djangoapps/discussion/rest_api/tests/test_tasks_v2.py @@ -0,0 +1,488 @@ +""" +Test cases for forum v2 based tasks.py +""" +from unittest import mock +from unittest.mock import Mock + +import ddt +import httpretty +from django.conf import settings +from edx_toggles.toggles.testutils import override_waffle_flag +from openedx_events.learning.signals import USER_NOTIFICATION_REQUESTED + +from common.djangoapps.student.models import CourseEnrollment +from common.djangoapps.student.tests.factories import UserFactory +from lms.djangoapps.discussion.rest_api.tasks import send_response_notifications +from lms.djangoapps.discussion.rest_api.tests.utils import ThreadMock +from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + +from ..discussions_notifications import DiscussionNotificationSender +from .test_views_v2 import DiscussionAPIViewTestMixin + + +def _get_mfe_url(course_id, post_id): + """ + get discussions mfe url to specific post. + """ + return f"{settings.DISCUSSIONS_MICROFRONTEND_URL}/{str(course_id)}/posts/{post_id}" + + +@ddt.ddt +@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) +class TestSendResponseNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestCase): + """ + Test for the send_response_notifications function + """ + + def setUp(self): + super().setUp() + httpretty.reset() + httpretty.enable() + self.addCleanup(httpretty.disable) + self.addCleanup(httpretty.reset) + + self.course = CourseFactory.create() + + # Patch 1 + patcher1 = mock.patch( + 'openedx.core.djangoapps.django_comment_common.comment_client.thread.is_forum_v2_enabled_for_thread', + autospec=True + ) + mock_forum_v2 = patcher1.start() + mock_forum_v2.return_value = (True, str(self.course.id)) + self.addCleanup(patcher1.stop) + + # Patch 2 + patcher2 = mock.patch( + 'openedx.core.djangoapps.discussions.config.waffle.ENABLE_FORUM_V2.is_enabled', + return_value=False + ) + patcher2.start() + self.addCleanup(patcher2.stop) + + # Patch 3 + patcher3 = mock.patch( + "openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread", + return_value=self.course.id + ) + self.mock_get_course_id_by_thread = patcher3.start() + self.addCleanup(patcher3.stop) + + # Patch 4 + patcher4 = mock.patch( + "openedx.core.djangoapps.django_comment_common.comment_client.models.forum_api.get_course_id_by_comment", + return_value=self.course.id + ) + self.mock_get_course_id_by_comment = patcher4.start() + self.addCleanup(patcher4.stop) + + # Patch 5 + patcher5 = mock.patch( + "openedx.core.djangoapps.django_comment_common.comment_client.models.is_forum_v2_enabled_for_comment", + return_value=(True, str(self.course.id)) + ) + self.mock_is_forum_v2_enabled_for_comment = patcher5.start() + self.addCleanup(patcher5.stop) + + self.user_1 = UserFactory.create() + CourseEnrollment.enroll(self.user_1, self.course.id) + self.user_2 = UserFactory.create() + CourseEnrollment.enroll(self.user_2, self.course.id) + self.user_3 = UserFactory.create() + CourseEnrollment.enroll(self.user_3, self.course.id) + self.thread = ThreadMock(thread_id=1, creator=self.user_1, title='test thread') + self.thread_2 = ThreadMock(thread_id=2, creator=self.user_2, title='test thread 2') + self.thread_3 = ThreadMock(thread_id=2, creator=self.user_1, title='test thread 3') + for thread in [self.thread_3, self.thread_2, self.thread]: + self.register_get_thread_response({ + 'id': thread.id, + 'course_id': str(self.course.id), + 'topic_id': 'abc', + "user_id": thread.user_id, + "username": thread.username, + "thread_type": 'discussion', + "title": thread.title, + "commentable_id": thread.commentable_id, + + }) + + self._register_subscriptions_endpoint() + + self.comment = ThreadMock(thread_id=4, creator=self.user_2, title='test comment', body='comment body') + self.register_get_comment_response( + { + 'id': self.comment.id, + 'thread_id': self.thread.id, + 'parent_id': None, + 'user_id': self.comment.user_id, + 'body': self.comment.body, + } + ) + + def test_basic(self): + """ + Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin + """ + + def test_not_authenticated(self): + """ + Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin + """ + + def test_send_notification_to_thread_creator(self): + """ + Test that the notification is sent to the thread creator + """ + handler = mock.Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + # Post the form or do what it takes to send the signal + send_response_notifications( + self.thread.id, + str(self.course.id), + self.user_2.id, + self.comment.id, + parent_id=None + ) + self.assertEqual(handler.call_count, 2) + args = handler.call_args_list[0][1]['notification_data'] + self.assertEqual([int(user_id) for user_id in args.user_ids], [self.user_1.id]) + self.assertEqual(args.notification_type, 'new_response') + expected_context = { + 'replier_name': self.user_2.username, + 'post_title': 'test thread', + 'email_content': self.comment.body, + 'course_name': self.course.display_name, + 'sender_id': self.user_2.id, + 'response_id': 4, + 'topic_id': None, + 'thread_id': 1, + 'comment_id': None, + } + self.assertDictEqual(args.context, expected_context) + self.assertEqual( + args.content_url, + _get_mfe_url(self.course.id, self.thread.id) + ) + self.assertEqual(args.app_name, 'discussion') + + def test_no_signal_on_creators_own_thread(self): + """ + Makes sure that 1 signal is emitted if user creates response on + their own thread. + """ + handler = mock.Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + send_response_notifications( + self.thread.id, + str(self.course.id), + self.user_1.id, + self.comment.id, parent_id=None + ) + self.assertEqual(handler.call_count, 1) + + @ddt.data( + (None, 'response_on_followed_post'), (1, 'comment_on_followed_post') + ) + @ddt.unpack + def test_send_notification_to_followers(self, parent_id, notification_type): + """ + Test that the notification is sent to the followers of the thread + """ + self.register_get_comment_response({ + 'id': self.thread.id, + 'thread_id': self.thread.id, + 'user_id': self.thread.user_id, + "body": "comment body" + }) + handler = Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + # Post the form or do what it takes to send the signal + notification_sender = DiscussionNotificationSender( + self.thread, + self.course, + self.user_2, + parent_id=parent_id, + comment_id=self.comment.id + ) + notification_sender.send_response_on_followed_post_notification() + self.assertEqual(handler.call_count, 1) + args = handler.call_args[1]['notification_data'] + # only sent to user_3 because user_2 is the one who created the response + self.assertEqual([self.user_3.id], args.user_ids) + self.assertEqual(args.notification_type, notification_type) + expected_context = { + 'replier_name': self.user_2.username, + 'post_title': 'test thread', + 'email_content': self.comment.body, + 'course_name': self.course.display_name, + 'sender_id': self.user_2.id, + 'response_id': 4 if notification_type == 'response_on_followed_post' else parent_id, + 'topic_id': None, + 'thread_id': 1, + 'comment_id': 4 if not notification_type == 'response_on_followed_post' else None, + } + if parent_id: + expected_context['author_name'] = 'dummy\'s' + expected_context['author_pronoun'] = 'dummy\'s' + self.assertDictEqual(args.context, expected_context) + self.assertEqual( + args.content_url, + _get_mfe_url(self.course.id, self.thread.id) + ) + self.assertEqual(args.app_name, 'discussion') + + def test_comment_creators_own_response(self): + """ + Check incase post author and response auther is same only send + new comment signal , with your as author_name. + """ + handler = mock.Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + self.register_get_comment_response({ + 'id': self.thread_3.id, + 'thread_id': self.thread.id, + 'user_id': self.thread_3.user_id, + 'body': 'comment body', + }) + + send_response_notifications( + self.thread.id, + str(self.course.id), + self.user_3.id, + parent_id=self.thread_2.id, + comment_id=self.comment.id + ) + # check if 1 call is made to the handler i.e. for the thread creator + self.assertEqual(handler.call_count, 2) + + # check if the notification is sent to the thread creator + args_comment = handler.call_args_list[0][1]['notification_data'] + self.assertEqual(args_comment.user_ids, [self.user_1.id]) + self.assertEqual(args_comment.notification_type, 'new_comment') + expected_context = { + 'replier_name': self.user_3.username, + 'post_title': self.thread.title, + 'author_name': 'dummy\'s', + 'author_pronoun': 'your', + 'course_name': self.course.display_name, + 'sender_id': self.user_3.id, + 'email_content': self.comment.body, + 'response_id': 2, + 'topic_id': None, + 'thread_id': 1, + 'comment_id': 4, + } + self.assertDictEqual(args_comment.context, expected_context) + self.assertEqual( + args_comment.content_url, + _get_mfe_url(self.course.id, self.thread.id) + ) + self.assertEqual(args_comment.app_name, 'discussion') + + def test_send_notification_to_parent_threads(self): + """ + Test that the notification signal is sent to the parent response creator and + parent thread creator, it checks signal is sent with correct arguments for both + types of notifications. + """ + handler = mock.Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + self.register_get_comment_response({ + 'id': self.thread_2.id, + 'thread_id': self.thread.id, + 'user_id': self.thread_2.user_id, + 'body': 'comment body' + }) + + send_response_notifications( + self.thread.id, + str(self.course.id), + self.user_3.id, + self.comment.id, + parent_id=self.thread_2.id + ) + # check if 2 call are made to the handler i.e. one for the response creator and one for the thread creator + self.assertEqual(handler.call_count, 2) + + # check if the notification is sent to the thread creator + args_comment = handler.call_args_list[0][1]['notification_data'] + args_comment_on_response = handler.call_args_list[1][1]['notification_data'] + self.assertEqual([int(user_id) for user_id in args_comment.user_ids], [self.user_1.id]) + self.assertEqual(args_comment.notification_type, 'new_comment') + expected_context = { + 'replier_name': self.user_3.username, + 'post_title': self.thread.title, + 'email_content': self.comment.body, + 'author_name': 'dummy\'s', + 'author_pronoun': 'dummy\'s', + 'course_name': self.course.display_name, + 'sender_id': self.user_3.id, + 'response_id': 2, + 'topic_id': None, + 'thread_id': 1, + 'comment_id': 4, + } + self.assertDictEqual(args_comment.context, expected_context) + self.assertEqual( + args_comment.content_url, + _get_mfe_url(self.course.id, self.thread.id) + ) + self.assertEqual(args_comment.app_name, 'discussion') + + # check if the notification is sent to the parent response creator + self.assertEqual([int(user_id) for user_id in args_comment_on_response.user_ids], [self.user_2.id]) + self.assertEqual(args_comment_on_response.notification_type, 'new_comment_on_response') + expected_context = { + 'replier_name': self.user_3.username, + 'post_title': self.thread.title, + 'email_content': self.comment.body, + 'course_name': self.course.display_name, + 'sender_id': self.user_3.id, + 'response_id': 2, + 'topic_id': None, + 'thread_id': 1, + 'comment_id': 4, + } + self.assertDictEqual(args_comment_on_response.context, expected_context) + self.assertEqual( + args_comment_on_response.content_url, + _get_mfe_url(self.course.id, self.thread.id) + ) + self.assertEqual(args_comment_on_response.app_name, 'discussion') + + def _register_subscriptions_endpoint(self): + """ + Registers the endpoint for the subscriptions API + """ + mock_response = { + 'collection': [ + { + '_id': 1, + 'subscriber_id': str(self.user_2.id), + "source_id": self.thread.id, + "source_type": "thread", + }, + { + '_id': 2, + 'subscriber_id': str(self.user_3.id), + "source_id": self.thread.id, + "source_type": "thread", + }, + ], + 'page': 1, + 'num_pages': 1, + 'subscriptions_count': 2, + 'corrected_text': None + + } + self.register_get_subscriptions(self.thread.id, mock_response) + + +@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) +class TestSendCommentNotification(DiscussionAPIViewTestMixin, ModuleStoreTestCase): + """ + Test case to send new_comment notification + """ + + def setUp(self): + super().setUp() + httpretty.reset() + httpretty.enable() + 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.is_forum_v2_enabled_for_thread', + autospec=True + ) + mock_forum_v2 = patcher.start() + mock_forum_v2.return_value = (True, str(self.course.id)) + self.addCleanup(patcher.stop) + + self.course = CourseFactory.create() + patcher = mock.patch( + "openedx.core.djangoapps.django_comment_common.comment_client.thread.forum_api.get_course_id_by_thread", + return_value=self.course.id + ) + self.mock_get_course_id_by_thread = 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", + return_value=self.course.id + ) + self.mock_get_course_id_by_comment = patcher.start() + self.addCleanup(patcher.stop) + + patcher = mock.patch( + "openedx.core.djangoapps.django_comment_common.comment_client.models.is_forum_v2_enabled_for_comment", + return_value=(True, str(self.course.id)) + ) + self.mock_is_forum_v2_enabled_for_comment = patcher.start() + self.addCleanup(patcher.stop) + + self.user_1 = UserFactory.create() + CourseEnrollment.enroll(self.user_1, self.course.id) + self.user_2 = UserFactory.create() + CourseEnrollment.enroll(self.user_2, self.course.id) + + def test_basic(self): + """ + Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin + """ + + def test_not_authenticated(self): + """ + Left empty intentionally. This test case is inherited from DiscussionAPIViewTestMixin + """ + + def test_new_comment_notification(self): + """ + Tests new comment notification generation + """ + handler = mock.Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + thread = ThreadMock(thread_id=1, creator=self.user_1, title='test thread') + response = ThreadMock(thread_id=2, creator=self.user_2, title='test response') + comment = ThreadMock(thread_id=3, creator=self.user_2, title='test comment', body='comment body') + self.register_get_thread_response({ + 'id': thread.id, + 'course_id': str(self.course.id), + 'topic_id': 'abc', + "user_id": thread.user_id, + "username": thread.username, + "thread_type": 'discussion', + "title": thread.title, + "commentable_id": thread.commentable_id, + + }) + self.register_get_comment_response({ + 'id': response.id, + 'thread_id': thread.id, + 'user_id': response.user_id + }) + self.register_get_comment_response({ + 'id': comment.id, + 'parent_id': response.id, + 'user_id': comment.user_id, + 'body': comment.body + }) + self.register_get_subscriptions(1, {}) + send_response_notifications(thread.id, str(self.course.id), self.user_2.id, parent_id=response.id, + comment_id=comment.id) + handler.assert_called_once() + context = handler.call_args[1]['notification_data'].context + self.assertEqual(context['author_name'], 'dummy\'s') + self.assertEqual(context['author_pronoun'], 'their') + self.assertEqual(context['email_content'], comment.body) diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/subscriptions.py b/openedx/core/djangoapps/django_comment_common/comment_client/subscriptions.py index 2130dfc56b..34814f6490 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/subscriptions.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/subscriptions.py @@ -5,7 +5,6 @@ import logging from . import models, settings, utils from forum import api as forum_api -from openedx.core.djangoapps.discussions.config.waffle import is_forum_v2_enabled log = logging.getLogger(__name__) @@ -36,22 +35,12 @@ class Subscription(models.Model): utils.strip_blank(utils.strip_none(query_params)) ) course_key = utils.get_course_key(course_id) - if is_forum_v2_enabled(course_key): - response = forum_api.get_thread_subscriptions( - thread_id=thread_id, - page=params["page"], - per_page=params["per_page"], - course_id=str(course_key) - ) - else: - response = utils.perform_request( - 'get', - cls.url(action='get', params=params) + "/subscriptions", - params, - metric_tags=[], - metric_action='subscription.get', - paged_results=True - ) + response = forum_api.get_thread_subscriptions( + thread_id=thread_id, + page=params["page"], + per_page=params["per_page"], + course_id=str(course_key) + ) return utils.SubscriptionsPaginatedResult( collection=response.get('collection', []), page=response.get('page', 1), diff --git a/openedx/core/djangoapps/django_comment_common/comment_client/user.py b/openedx/core/djangoapps/django_comment_common/comment_client/user.py index ee9591e51d..de676b77a8 100644 --- a/openedx/core/djangoapps/django_comment_common/comment_client/user.py +++ b/openedx/core/djangoapps/django_comment_common/comment_client/user.py @@ -52,39 +52,19 @@ class User(models.Model): def follow(self, source, course_id=None): course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) - if is_forum_v2_enabled(course_key): - forum_api.create_subscription( - user_id=self.id, - source_id=source.id, - course_id=str(course_key) - ) - else: - params = {'source_type': source.type, 'source_id': source.id} - utils.perform_request( - 'post', - _url_for_subscription(self.id), - params, - metric_action='user.follow', - metric_tags=self._metric_tags + [f'target.type:{source.type}'], - ) + forum_api.create_subscription( + user_id=self.id, + source_id=source.id, + course_id=str(course_key) + ) def unfollow(self, source, course_id=None): course_key = utils.get_course_key(self.attributes.get("course_id") or course_id) - if is_forum_v2_enabled(course_key): - forum_api.delete_subscription( - user_id=self.id, - source_id=source.id, - course_id=str(course_key) - ) - else: - params = {'source_type': source.type, 'source_id': source.id} - utils.perform_request( - 'delete', - _url_for_subscription(self.id), - params, - metric_action='user.unfollow', - metric_tags=self._metric_tags + [f'target.type:{source.type}'], - ) + forum_api.delete_subscription( + user_id=self.id, + source_id=source.id, + course_id=str(course_key) + ) def vote(self, voteable, value, course_id=None): course_key = utils.get_course_key(self.attributes.get("course_id") or course_id)