diff --git a/lms/djangoapps/discussion/django_comment_client/base/views.py b/lms/djangoapps/discussion/django_comment_client/base/views.py index f0c79f1fad..2fe5859e11 100644 --- a/lms/djangoapps/discussion/django_comment_client/base/views.py +++ b/lms/djangoapps/discussion/django_comment_client/base/views.py @@ -6,6 +6,7 @@ import random import time import eventtracking + import six from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user @@ -191,6 +192,39 @@ def track_thread_lock_unlock_event(request, course, thread, close_reason_code, l track_forum_event(request, event_name, course, thread, event_data) +def track_thread_edited_event(request, course, thread, edit_reason_code): + """ + Send analytics event for an edited thread. + """ + event_name = _EVENT_NAME_TEMPLATE.format(obj_type='thread', action_name='edited') + own_content = str(request.user.id) == thread['user_id'] + event_data = { + 'target_username': thread.get('username'), + 'content_type': 'Post', + 'own_content': own_content, + 'edit_reason': edit_reason_code, + 'commentable_id': thread.get('commentable_id', ''), + } + track_forum_event(request, event_name, course, thread, event_data) + + +def track_comment_edited_event(request, course, comment, edit_reason_code): + """ + Send analytics event for an edited response or comment. + """ + obj_type = 'comment' if comment.get('parent_id') else 'response' + event_name = _EVENT_NAME_TEMPLATE.format(obj_type=obj_type, action_name='edited') + own_content = str(request.user.id) == comment['user_id'] + event_data = { + 'target_username': comment.get('username'), + 'own_content': own_content, + 'content_type': obj_type.capitalize(), + 'edit_reason': edit_reason_code, + 'commentable_id': comment.get('commentable_id', ''), + } + track_forum_event(request, event_name, course, comment, event_data) + + def track_thread_deleted_event(request, course, thread): """ Send analytics event for a deleted thread. diff --git a/lms/djangoapps/discussion/rest_api/serializers.py b/lms/djangoapps/discussion/rest_api/serializers.py index e5f68ef474..f3e361431f 100644 --- a/lms/djangoapps/discussion/rest_api/serializers.py +++ b/lms/djangoapps/discussion/rest_api/serializers.py @@ -15,7 +15,8 @@ from rest_framework import serializers from common.djangoapps.student.models import get_user_by_username_or_email from common.djangoapps.student.roles import GlobalStaff -from lms.djangoapps.discussion.django_comment_client.base.views import track_thread_lock_unlock_event +from lms.djangoapps.discussion.django_comment_client.base.views import track_thread_lock_unlock_event, \ + track_thread_edited_event, track_comment_edited_event from lms.djangoapps.discussion.django_comment_client.utils import ( course_discussion_division_enabled, get_group_id_for_user, @@ -437,26 +438,18 @@ class ThreadSerializer(_ContentSerializer): requesting_user_id = self.context["cc_requester"]["id"] if key == "closed" and val: instance["closing_user_id"] = requesting_user_id - event_data = { - 'request': self.context['request'], - 'course': self.context['course'], - 'thread': instance, - 'close_reason_code': validated_data.get('close_reason_code') - } - track_thread_lock_unlock_event(**event_data) + track_thread_lock_unlock_event(self.context['request'], self.context['course'], + instance, validated_data.get('close_reason_code')) if key == "closed" and not val: instance["closing_user_id"] = requesting_user_id - event_data = { - 'request': self.context['request'], - 'course': self.context['course'], - 'thread': instance, - 'close_reason_code': validated_data.get('close_reason_code') - } - track_thread_lock_unlock_event(**event_data, locked=False) + track_thread_lock_unlock_event(self.context['request'], self.context['course'], + instance, validated_data.get('close_reason_code'), locked=False) if key == "body" and val: instance["editing_user_id"] = requesting_user_id + track_thread_edited_event(self.context['request'], self.context['course'], + instance, validated_data.get('edit_reason_code')) instance.save() return instance @@ -601,6 +594,8 @@ class CommentSerializer(_ContentSerializer): instance["endorsement_user_id"] = requesting_user_id if key == "body" and val: instance["editing_user_id"] = requesting_user_id + track_comment_edited_event(self.context['request'], self.context['course'], + instance, validated_data.get('edit_reason_code')) instance.save() return instance diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api.py b/lms/djangoapps/discussion/rest_api/tests/test_api.py index 72c4a3f922..a36ba5580a 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api.py @@ -2867,7 +2867,8 @@ class UpdateThreadTest( @mock.patch("lms.djangoapps.discussion.rest_api.serializers.EDIT_REASON_CODES", { "test-edit-reason": "Test Edit Reason", }) - def test_update_thread_with_edit_reason_code(self, role_name): + @mock.patch("eventtracking.tracker.emit") + def test_update_thread_with_edit_reason_code(self, role_name, mock_emit): """ Test editing comments, specifying and retrieving edit reason codes. """ @@ -2887,6 +2888,24 @@ class UpdateThreadTest( } request_body = httpretty.last_request().parsed_body # pylint: disable=no-member assert request_body["edit_reason_code"] == ["test-edit-reason"] + + expected_event_name = 'edx.forum.thread.edited' + expected_event_data = { + 'id': 'test_thread', + 'content_type': 'Post', + 'own_content': False, + 'url': '', + 'user_course_roles': [], + 'user_forums_roles': ['Student', role_name], + 'target_username': self.user.username, + 'edit_reason': 'test-edit-reason', + 'commentable_id': 'original_topic' + } + + 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) + except ValidationError as error: assert role_name == FORUM_ROLE_STUDENT assert error.message_dict == {"edit_reason_code": ["This field is not editable."], @@ -2915,7 +2934,7 @@ class UpdateThreadTest( _assign_role_to_user(user=self.user, course_id=self.course.id, role=role_name) self.register_thread() try: - self.request.META['HTTP_REFERER'] = 'https://example.cop' + self.request.META['HTTP_REFERER'] = 'https://example.com' result = update_thread(self.request, "test_thread", { "closed": closed, "close_reason_code": "test-close-reason", @@ -3368,7 +3387,8 @@ class UpdateCommentTest( @mock.patch("lms.djangoapps.discussion.rest_api.serializers.EDIT_REASON_CODES", { "test-edit-reason": "Test Edit Reason", }) - def test_update_comment_with_edit_reason_code(self, role_name): + @mock.patch("eventtracking.tracker.emit") + def test_update_comment_with_edit_reason_code(self, role_name, mock_emit): """ Test editing comments, specifying and retrieving edit reason codes. """ @@ -3388,6 +3408,24 @@ class UpdateCommentTest( } request_body = httpretty.last_request().parsed_body # pylint: disable=no-member assert request_body["edit_reason_code"] == ["test-edit-reason"] + + expected_event_name = 'edx.forum.response.edited' + expected_event_data = { + 'id': 'test_comment', + 'content_type': 'Response', + 'own_content': False, + 'url': '', + 'user_course_roles': [], + 'user_forums_roles': ['Student', role_name], + 'target_username': self.user.username, + 'edit_reason': 'test-edit-reason', + 'commentable_id': 'dummy' + } + + 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) + except ValidationError: assert role_name == FORUM_ROLE_STUDENT