From 9e6502474482b8c5310ac069bd58f813fa3be73c Mon Sep 17 00:00:00 2001 From: Cristhian Garcia Date: Mon, 30 Oct 2023 10:46:39 -0500 Subject: [PATCH] feat: emit signal for thread, response, and comment created events (#33395) --- .../django_comment_client/base/tests.py | 61 ++++++++++++++++++- .../django_comment_client/base/views.py | 51 +++++++++++++++- 2 files changed, 109 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/discussion/django_comment_client/base/tests.py b/lms/djangoapps/discussion/django_comment_client/base/tests.py index b871d4af44a9..bc4e702886c9 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/tests.py +++ b/lms/djangoapps/discussion/django_comment_client/base/tests.py @@ -16,6 +16,7 @@ from eventtracking.processors.exceptions import EventEmissionExit from mock import ANY, Mock, patch from opaque_keys.edx.keys import CourseKey +from openedx_events.learning.signals import FORUM_THREAD_CREATED, FORUM_THREAD_RESPONSE_CREATED, FORUM_RESPONSE_COMMENT_CREATED from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.course_modes.tests.factories import CourseModeFactory @@ -406,7 +407,7 @@ def inner(self, default_store, block_count, mongo_calls, sql_queries, *args, **k return inner @ddt.data( - (ModuleStoreEnum.Type.split, 3, 8, 42), + (ModuleStoreEnum.Type.split, 3, 8, 43), ) @ddt.unpack @count_queries @@ -1735,6 +1736,8 @@ def test_response_event(self, 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) self._set_mock_request_data(mock_request, { "closed": False, "commentable_id": 'test_commentable_id', @@ -1754,12 +1757,29 @@ def test_response_event(self, mock_request, mock_emit): 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) def test_comment_event(self, mock_request, mock_emit): """ Ensure an event is fired when someone comments on a response. """ + event_receiver = Mock() + FORUM_RESPONSE_COMMENT_CREATED.connect(event_receiver) self._set_mock_request_data(mock_request, { "closed": False, "depth": 1, @@ -1781,6 +1801,19 @@ def test_comment_event(self, mock_request, mock_emit): assert event['user_course_roles'] == ['Wizard'] assert event['options']['followed'] is False + self.assertDictContainsSubset( + { + "signal": FORUM_RESPONSE_COMMENT_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) @ddt.data(( @@ -1809,6 +1842,10 @@ def test_team_events(self, view_name, event_name, view_data, view_kwargs, mock_r 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) + self._set_mock_request_data(mock_request, { 'closed': False, 'commentable_id': TEAM_COMMENTABLE_ID, @@ -1825,6 +1862,19 @@ def test_team_events(self, view_name, event_name, view_data, view_kwargs, mock_r 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( ('vote_for_thread', 'thread_id', 'thread'), ('undo_vote_for_thread', 'thread_id', 'thread'), @@ -1863,6 +1913,10 @@ def test_thread_voted_event(self, view_name, obj_id_name, obj_type, mock_request @patch('eventtracking.tracker.emit') @patch('openedx.core.djangoapps.django_comment_common.comment_client.utils.requests.request', autospec=True) def test_thread_followed_event(self, view_name, mock_request, mock_emit): + event_receiver = Mock() + for signal in views.TRACKING_LOG_TO_EVENT_MAPS.values(): + signal.connect(event_receiver) + self._set_mock_request_data(mock_request, { 'closed': False, 'commentable_id': 'test_commentable_id', @@ -1887,6 +1941,11 @@ def test_thread_followed_event(self, view_name, mock_request, mock_emit): 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/views.py b/lms/djangoapps/discussion/django_comment_client/base/views.py index 1ec41e31551e..f81322a2380e 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/views.py +++ b/lms/djangoapps/discussion/django_comment_client/base/views.py @@ -16,12 +16,18 @@ from django.views.decorators.http import require_GET, require_POST from eventtracking import tracker from opaque_keys.edx.keys import CourseKey +from openedx_events.learning.data import DiscussionThreadData, UserData, UserPersonalData +from openedx_events.learning.signals import ( + FORUM_RESPONSE_COMMENT_CREATED, + FORUM_THREAD_CREATED, + FORUM_THREAD_RESPONSE_CREATED +) import lms.djangoapps.discussion.django_comment_client.settings as cc_settings import openedx.core.djangoapps.django_comment_common.comment_client as cc from common.djangoapps.student.roles import GlobalStaff -from common.djangoapps.util.file import store_uploaded_file from common.djangoapps.track import contexts +from common.djangoapps.util.file import store_uploaded_file from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.courses import get_course_overview_with_access, get_course_with_access from lms.djangoapps.courseware.exceptions import CourseAccessRedirect @@ -42,7 +48,7 @@ get_user_group_ids, is_comment_too_deep, prepare_content, - sanitize_body, + sanitize_body ) from openedx.core.djangoapps.django_comment_common.signals import ( comment_created, @@ -66,6 +72,12 @@ TRACKING_MAX_FORUM_TITLE = 1000 _EVENT_NAME_TEMPLATE = 'edx.forum.{obj_type}.{action_name}' +TRACKING_LOG_TO_EVENT_MAPS = { + 'edx.forum.thread.created': FORUM_THREAD_CREATED, + 'edx.forum.response.created': FORUM_THREAD_RESPONSE_CREATED, + 'edx.forum.comment.created': FORUM_RESPONSE_COMMENT_CREATED, +} + def track_forum_event(request, event_name, course, obj, data, id_map=None): """ @@ -97,6 +109,41 @@ def track_forum_event(request, event_name, course, obj, data, id_map=None): with tracker.get_tracker().context(event_name, context): tracker.emit(event_name, data) + forum_event = TRACKING_LOG_TO_EVENT_MAPS.get(event_name, None) + if forum_event is not None: + forum_event.send_event( + thread=DiscussionThreadData( + anonymous=data.get('anonymous'), + anonymous_to_peers=data.get('anonymous_to_peers'), + body=data.get('body'), + category_id=data.get('category_id'), + category_name=data.get('category_name'), + commentable_id=data.get('commentable_id'), + group_id=data.get('group_id'), + id=data.get('id'), + team_id=data.get('team_id'), + thread_type=data.get('thread_type'), + title=data.get('title'), + title_truncated=data.get('title_truncated'), + truncated=data.get('truncated'), + url=data.get('url'), + discussion=data.get('discussion'), + user_course_roles=data.get('user_course_roles'), + user_forums_roles=data.get('user_forums_roles'), + user=UserData( + pii=UserPersonalData( + username=user.username, + email=user.email, + name=user.profile.name, + ), + id=user.id, + is_active=user.is_active, + ), + course_id=str(course.id), + options=data.get('options'), + ) + ) + def track_created_event(request, event_name, course, obj, data): """