diff --git a/lms/djangoapps/discussion/rest_api/discussions_notifications.py b/lms/djangoapps/discussion/rest_api/discussions_notifications.py index a148fbdc9f2a..da5b430fcef0 100644 --- a/lms/djangoapps/discussion/rest_api/discussions_notifications.py +++ b/lms/djangoapps/discussion/rest_api/discussions_notifications.py @@ -235,6 +235,22 @@ def _create_cohort_course_audience(self): } return {} + def send_response_endorsed_on_thread_notification(self): + """ + Sends a notification to the author of the thread + response on his thread has been endorsed + """ + context = { + "username": self.creator.username, + } + self._send_notification([self.thread.user_id], "response_endorsed_on_thread", context) + + def send_response_endorsed_notification(self): + """ + Sends a notification to the author of the response + """ + self._send_notification([self.creator.id], "response_endorsed") + def send_new_thread_created_notification(self): """ Send notification based on notification_type diff --git a/lms/djangoapps/discussion/rest_api/tasks.py b/lms/djangoapps/discussion/rest_api/tasks.py index 385e2a1f46a0..bd41e1078cd3 100644 --- a/lms/djangoapps/discussion/rest_api/tasks.py +++ b/lms/djangoapps/discussion/rest_api/tasks.py @@ -47,3 +47,22 @@ def send_response_notifications(thread_id, course_key_str, user_id, parent_id=No notification_sender.send_new_response_notification() notification_sender.send_new_comment_on_response_notification() notification_sender.send_response_on_followed_post_notification() + + +@shared_task +@set_code_owner_attribute +def send_response_endorsed_notifications(thread_id, course_key_str, comment_author_id): + """ + Send notifications when a response is marked answered/ endorsed + """ + course_key = CourseKey.from_string(course_key_str) + if not ENABLE_NOTIFICATIONS.is_enabled(course_key): + return + thread = Thread(id=thread_id).retrieve() + comment_author = User.objects.get(id=comment_author_id) + course = get_course_with_access(comment_author, 'load', course_key, check_if_enrolled=True) + notification_sender = DiscussionNotificationSender(thread, course, comment_author) + #sends notification to author of thread + notification_sender.send_response_endorsed_on_thread_notification() + #sends notification to author of response + notification_sender.send_response_endorsed_notification() diff --git a/lms/djangoapps/discussion/rest_api/tests/test_tasks.py b/lms/djangoapps/discussion/rest_api/tests/test_tasks.py index 9e5a5bc79a9f..acb8da294ff6 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_tasks.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_tasks.py @@ -13,7 +13,10 @@ from common.djangoapps.student.models import CourseEnrollment 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_notifications, send_thread_created_notification +from lms.djangoapps.discussion.rest_api.tasks import ( + send_response_notifications, + send_thread_created_notification, + send_response_endorsed_notifications) from lms.djangoapps.discussion.rest_api.tests.utils import ThreadMock, make_minimal_cs_thread from openedx.core.djangoapps.course_groups.models import CohortMembership, CourseCohortsSettings from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory @@ -49,6 +52,7 @@ class TestNewThreadCreatedNotification(DiscussionAPIViewTestMixin, ModuleStoreTe """ Test cases related to new_discussion_post and new_question_post notification types """ + def setUp(self): """ Setup test case @@ -478,6 +482,7 @@ class TestSendCommentNotification(DiscussionAPIViewTestMixin, ModuleStoreTestCas """ Test case to send new_comment notification """ + def setUp(self): super().setUp() httpretty.reset() @@ -527,3 +532,92 @@ def test_new_comment_notification(self): handler.assert_called_once() context = handler.call_args[1]['notification_data'].context self.assertEqual(context['author_name'], 'their') + + +@override_waffle_flag(ENABLE_NOTIFICATIONS, active=True) +class TestResponseEndorsedNotifications(DiscussionAPIViewTestMixin, ModuleStoreTestCase): + """ + Test case to send response endorsed notifications + """ + + def setUp(self): + super().setUp() + httpretty.reset() + httpretty.enable() + + self.course = CourseFactory.create() + 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_response_endorsed_notifications(self): + """ + Tests nresponse endorsed notifications + """ + + thread = ThreadMock(thread_id=1, creator=self.user_1, title='test thread') + response = ThreadMock(thread_id=2, creator=self.user_2, title='test response') + 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, + }) + self.register_get_comment_response({ + 'id': response.id, + 'thread_id': thread.id, + 'user_id': response.user_id + }) + handler = mock.Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + send_response_endorsed_notifications(thread.id, str(self.course.id), self.user_2.id) + self.assertEqual(handler.call_count, 2) + + #Test response endorsed on thread notification + notification_data = handler.call_args_list[0][1]['notification_data'] + # Target only the thread author + self.assertEqual([int(user_id) for user_id in notification_data.user_ids], [int(thread.user_id)]) + self.assertEqual(notification_data.notification_type, 'response_endorsed_on_thread') + + expected_context = { + 'replier_name': response.username, + 'post_title': 'test thread', + 'course_name': self.course.display_name, + 'sender_id': int(response.user_id), + 'username': response.username, + } + self.assertDictEqual(notification_data.context, expected_context) + self.assertEqual(notification_data.content_url, _get_mfe_url(self.course.id, thread.id)) + self.assertEqual(notification_data.app_name, 'discussion') + self.assertEqual('response_endorsed_on_thread', notification_data.notification_type) + + #Test response endorsed notification + notification_data = handler.call_args_list[1][1]['notification_data'] + # Target only the response author + self.assertEqual([int(user_id) for user_id in notification_data.user_ids], [int(response.user_id)]) + self.assertEqual(notification_data.notification_type, 'response_endorsed') + + expected_context = { + 'replier_name': response.username, + 'post_title': 'test thread', + 'course_name': self.course.display_name, + 'sender_id': int(response.user_id), + } + self.assertDictEqual(notification_data.context, expected_context) + self.assertEqual(notification_data.content_url, _get_mfe_url(self.course.id, thread.id)) + self.assertEqual(notification_data.app_name, 'discussion') + self.assertEqual('response_endorsed', notification_data.notification_type) diff --git a/lms/djangoapps/discussion/signals/handlers.py b/lms/djangoapps/discussion/signals/handlers.py index f857e1aa8c82..423bc5b2bc7e 100644 --- a/lms/djangoapps/discussion/signals/handlers.py +++ b/lms/djangoapps/discussion/signals/handlers.py @@ -15,7 +15,11 @@ from xmodule.modulestore.django import SignalHandler, modulestore from lms.djangoapps.discussion import tasks -from lms.djangoapps.discussion.rest_api.tasks import send_response_notifications, send_thread_created_notification +from lms.djangoapps.discussion.rest_api.tasks import ( + send_response_notifications, + send_thread_created_notification, + send_response_endorsed_notifications +) from openedx.core.djangoapps.django_comment_common import signals from openedx.core.djangoapps.site_configuration.models import SiteConfiguration from openedx.core.djangoapps.theming.helpers import get_current_site @@ -178,3 +182,17 @@ def create_comment_created_notification(*args, **kwargs): parent_id = comment.attributes['parent_id'] course_key_str = comment.attributes['course_id'] send_response_notifications.apply_async(args=[thread_id, course_key_str, user.id, parent_id]) + + +@receiver(signals.comment_endorsed) +def create_response_endorsed_on_thread_notification(*args, **kwargs): + """ + Creates a notification for thread author when response on thread is endorsed + and another notification for response author when response is endorsed + """ + comment = kwargs['post'] + comment_author_id = comment.attributes['user_id'] + thread_id = comment.attributes['thread_id'] + course_key_str = comment.attributes['course_id'] + + send_response_endorsed_notifications.apply_async(args=[thread_id, course_key_str, comment_author_id]) diff --git a/lms/djangoapps/teams/tests/test_models.py b/lms/djangoapps/teams/tests/test_models.py index 02e238bf2a54..60190076004c 100644 --- a/lms/djangoapps/teams/tests/test_models.py +++ b/lms/djangoapps/teams/tests/test_models.py @@ -320,8 +320,11 @@ def test_signals(self, signal_name, user_should_update): user = getattr(self, user) with patch('lms.djangoapps.discussion.rest_api.tasks.send_response_notifications.apply_async'): with patch('lms.djangoapps.discussion.rest_api.tasks.send_thread_created_notification.apply_async'): - signal = self.SIGNALS[signal_name] - signal.send(sender=None, user=user, post=self.mock_comment()) + with patch( + 'lms.djangoapps.discussion.rest_api.tasks.send_response_endorsed_notifications.apply_async' + ): + signal = self.SIGNALS[signal_name] + signal.send(sender=None, user=user, post=self.mock_comment()) @ddt.data('thread_voted', 'comment_voted') def test_vote_others_post(self, signal_name): @@ -339,5 +342,8 @@ def test_signals_course_context(self, signal_name): with self.assert_last_activity_updated(False): with patch('lms.djangoapps.discussion.rest_api.tasks.send_response_notifications.apply_async'): with patch('lms.djangoapps.discussion.rest_api.tasks.send_thread_created_notification.apply_async'): - signal = self.SIGNALS[signal_name] - signal.send(sender=None, user=self.user, post=self.mock_comment(context='course')) + with patch( + 'lms.djangoapps.discussion.rest_api.tasks.send_response_endorsed_notifications.apply_async' + ): + signal = self.SIGNALS[signal_name] + signal.send(sender=None, user=self.user, post=self.mock_comment(context='course')) diff --git a/openedx/core/djangoapps/notifications/base_notification.py b/openedx/core/djangoapps/notifications/base_notification.py index 9d620b5b6187..946314cfa0bd 100644 --- a/openedx/core/djangoapps/notifications/base_notification.py +++ b/openedx/core/djangoapps/notifications/base_notification.py @@ -132,6 +132,34 @@ }, 'email_template': '', }, + 'response_endorsed_on_thread': { + 'notification_app': 'discussion', + 'name': 'response_endorsed_on_thread', + 'is_core': True, + 'info': '', + 'non_editable': [], + 'content_template': _('<{p}><{strong}>{username} response has been endorsed in your post ' + '<{strong}>{post_title}'), + 'content_context': { + 'post_title': 'Post title', + 'username': 'Response author name', + }, + 'email_template': '', + 'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE] + }, + 'response_endorsed': { + 'notification_app': 'discussion', + 'name': 'response_endorsed', + 'is_core': True, + 'info': '', + 'non_editable': [], + 'content_template': _('<{p}>{post_title}'), + 'content_context': { + 'post_title': 'Post title', + }, + 'email_template': '', + 'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE] + } } COURSE_NOTIFICATION_APPS = { diff --git a/openedx/core/djangoapps/notifications/models.py b/openedx/core/djangoapps/notifications/models.py index c2c03f202352..1014683f062c 100644 --- a/openedx/core/djangoapps/notifications/models.py +++ b/openedx/core/djangoapps/notifications/models.py @@ -21,7 +21,7 @@ NOTIFICATION_CHANNELS = ['web', 'push', 'email'] # Update this version when there is a change to any course specific notification type or app. -COURSE_NOTIFICATION_CONFIG_VERSION = 5 +COURSE_NOTIFICATION_CONFIG_VERSION = 6 def get_course_notification_preference_config(): diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index 815266235b23..b094a974aaaa 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -236,7 +236,9 @@ def _expected_api_response(self, course=None): 'new_comment', 'new_response', 'response_on_followed_post', - 'comment_on_followed_post' + 'comment_on_followed_post', + 'response_endorsed_on_thread', + 'response_endorsed' ], 'notification_types': { 'core': {