diff --git a/openedx/core/djangoapps/notifications/events.py b/openedx/core/djangoapps/notifications/events.py new file mode 100644 index 0000000000..11166b841d --- /dev/null +++ b/openedx/core/djangoapps/notifications/events.py @@ -0,0 +1,105 @@ +""" Events for notification app. """ + +from eventtracking import tracker +from common.djangoapps.track import contexts + + +NOTIFICATION_PREFERENCES_VIEWED = 'edx.notifications.preferences.viewed' +NOTIFICATION_GENERATED = 'edx.notifications.generated' +NOTIFICATION_READ = 'edx.notifications.read' +NOTIFICATION_PREFERENCES_UPDATED = 'edx.notifications.preferences.updated' + + +def get_user_forums_roles(user, course_id): + """ + Get the user's roles in the course forums. + """ + if course_id: + return list(user.roles.filter(course_id=course_id).values_list('name', flat=True)) + return [] + + +def get_user_course_roles(user, course_id): + """ + Get the user's roles in the course. + """ + if course_id: + return list(user.courseaccessrole_set.filter(course_id=course_id).values_list('role', flat=True)) + return [] + + +def notification_event_context(user, course_id, notification): + return { + 'user_id': str(user.id), + 'course_id': str(course_id), + 'notification_type': notification.notification_type, + 'notification_app': notification.app_name, + 'notification_metadata': { + 'notification_id': notification.id, + 'notification_content': notification.content, + }, + 'user_forum_roles': get_user_forums_roles(user, course_id), + 'user_course_roles': get_user_course_roles(user, course_id), + } + + +def notification_preferences_viewed_event(request, course_id): + """ + Emit an event when a user views their notification preferences. + """ + context = contexts.course_context_from_course_id(course_id) + with tracker.get_tracker().context(NOTIFICATION_PREFERENCES_VIEWED, context): + tracker.emit( + NOTIFICATION_PREFERENCES_VIEWED, + { + 'user_id': str(request.user.id), + 'course_id': str(course_id), + 'user_forum_roles': get_user_forums_roles(request.user, course_id), + 'user_course_roles': get_user_course_roles(request.user, course_id), + } + ) + + +def notification_generated_event(user, notification): + """ + Emit an event when a notification is generated. + """ + context = contexts.course_context_from_course_id(notification.course_id) + with tracker.get_tracker().context(NOTIFICATION_GENERATED, context): + tracker.emit( + NOTIFICATION_GENERATED, + notification_event_context(user, notification.course_id, notification) + ) + + +def notification_read_event(user, notification): + """ + Emit an event when a notification is read. + """ + context = contexts.course_context_from_course_id(notification.course_id) + with tracker.get_tracker().context(NOTIFICATION_READ, context): + tracker.emit( + NOTIFICATION_READ, + notification_event_context(user, notification.course_id, notification) + ) + + +def notification_preference_update_event(user, course_id, updated_preference): + """ + Emit an event when a notification preference is updated. + """ + context = contexts.course_context_from_course_id(course_id) + with tracker.get_tracker().context(NOTIFICATION_PREFERENCES_UPDATED, context): + tracker.emit( + NOTIFICATION_PREFERENCES_UPDATED, + { + 'user_id': str(user.id), + 'course_id': str(course_id), + 'user_forum_roles': get_user_forums_roles(user, course_id), + 'user_course_roles': get_user_course_roles(user, course_id), + 'notification_app': updated_preference.get('notification_app', ''), + 'notification_type': updated_preference.get('notification_type', ''), + 'notification_channel': updated_preference.get('notification_channel', ''), + 'value': updated_preference.get('value', ''), + } + ) diff --git a/openedx/core/djangoapps/notifications/tasks.py b/openedx/core/djangoapps/notifications/tasks.py index fe8251e318..477cbbc0a0 100644 --- a/openedx/core/djangoapps/notifications/tasks.py +++ b/openedx/core/djangoapps/notifications/tasks.py @@ -19,6 +19,7 @@ from openedx.core.djangoapps.notifications.models import ( Notification, get_course_notification_preference_config_version ) +from openedx.core.djangoapps.notifications.events import notification_generated_event logger = get_task_logger(__name__) @@ -100,14 +101,16 @@ def send_notifications(user_ids, course_key: str, app_name, notification_type, c for preference in preferences: preference = update_user_preference(preference, preference.user, course_key) if preference and preference.get_web_config(app_name, notification_type): - notifications.append(Notification( + notification = Notification( user_id=preference.user_id, app_name=app_name, notification_type=notification_type, content_context=context, content_url=content_url, course_id=course_key, - )) + ) + notifications.append(notification) + notification_generated_event(preference.user, notification) # send notification to users but use bulk_create Notification.objects.bulk_create(notifications) diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index ec3dbe4dcb..9127070289 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -3,6 +3,7 @@ Tests for the views in the notifications app. """ import json from datetime import datetime, timedelta +from unittest import mock import ddt from django.conf import settings @@ -245,7 +246,8 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): response = self.client.get(self.path) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_get_user_notification_preference(self): + @mock.patch("eventtracking.tracker.emit") + def test_get_user_notification_preference(self, mock_emit): """ Test get user notification preference. """ @@ -253,6 +255,8 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): response = self.client.get(self.path) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, self._expected_api_response()) + event_name, event_data = mock_emit.call_args[0] + self.assertEqual(event_name, 'edx.notifications.preferences.viewed') @ddt.data( ('discussion', None, None, True, status.HTTP_200_OK, 'app_update'), @@ -269,8 +273,9 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): ('discussion', 'new_comment', 'invalid_notification_channel', False, status.HTTP_400_BAD_REQUEST, None), ) @ddt.unpack + @mock.patch("eventtracking.tracker.emit") def test_patch_user_notification_preference( - self, notification_app, notification_type, notification_channel, value, expected_status, update_type, + self, notification_app, notification_type, notification_channel, value, expected_status, update_type, mock_emit, ): """ Test update of user notification preference. @@ -299,6 +304,14 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): 'notification_types'][notification_type][notification_channel] = value self.assertEqual(response.data, expected_data) + if expected_status == status.HTTP_200_OK: + event_name, event_data = mock_emit.call_args[0] + self.assertEqual(event_name, 'edx.notifications.preferences.updated') + self.assertEqual(event_data['notification_app'], notification_app) + self.assertEqual(event_data['notification_type'], notification_type or '') + self.assertEqual(event_data['notification_channel'], notification_channel or '') + self.assertEqual(event_data['value'], value) + class NotificationListAPIViewTest(APITestCase): """ @@ -591,7 +604,8 @@ class NotificationReadAPIViewTestCase(APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data, {'error': 'Invalid app_name or notification_id.'}) - def test_mark_notification_read_with_notification_id(self): + @mock.patch("eventtracking.tracker.emit") + def test_mark_notification_read_with_notification_id(self, mock_emit): # Create a PATCH request to mark notification as read for notification_id: 2 notification_id = 2 data = {'notification_id': notification_id} @@ -602,6 +616,11 @@ class NotificationReadAPIViewTestCase(APITestCase): self.assertEqual(response.data, {'message': 'Notification marked read.'}) notifications = Notification.objects.filter(user=self.user, id=notification_id, last_read__isnull=False) self.assertEqual(notifications.count(), 1) + event_name, event_data = mock_emit.call_args[0] + self.assertEqual(event_name, 'edx.notifications.read') + self.assertEqual(event_data.get('notification_metadata').get('notification_id'), notification_id) + self.assertEqual(event_data['notification_app'], 'discussion') + self.assertEqual(event_data['notification_type'], 'Type A') def test_mark_notification_read_with_other_user_notification_id(self): # Create a PATCH request to mark notification as read for notification_id: 2 through a different user diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index 83838bd6c4..5119086683 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -22,6 +22,7 @@ from openedx.core.djangoapps.notifications.models import ( from .base_notification import COURSE_NOTIFICATION_APPS from .config.waffle import ENABLE_NOTIFICATIONS, SHOW_NOTIFICATIONS_TRAY +from .events import notification_preferences_viewed_event, notification_read_event, notification_preference_update_event from .models import Notification from .serializers import ( NotificationCourseEnrollmentSerializer, @@ -163,6 +164,7 @@ class UserNotificationPreferenceView(APIView): course_id = CourseKey.from_string(course_key_string) user_preference = CourseNotificationPreference.get_updated_user_course_preferences(request.user, course_id) serializer = UserCourseNotificationPreferenceSerializer(user_preference) + notification_preferences_viewed_event(request, course_id) return Response(serializer.data) def patch(self, request, course_key_string): @@ -191,11 +193,12 @@ class UserNotificationPreferenceView(APIView): status=status.HTTP_409_CONFLICT, ) - preference_update_serializer = UserNotificationPreferenceUpdateSerializer( + preference_update = UserNotificationPreferenceUpdateSerializer( user_course_notification_preference, data=request.data, partial=True ) - preference_update_serializer.is_valid(raise_exception=True) - updated_notification_preferences = preference_update_serializer.save() + preference_update.is_valid(raise_exception=True) + updated_notification_preferences = preference_update.save() + notification_preference_update_event(request.user, course_id, preference_update.validated_data) serializer = UserCourseNotificationPreferenceSerializer(updated_notification_preferences) return Response(serializer.data, status=status.HTTP_200_OK) @@ -387,6 +390,7 @@ class NotificationReadAPIView(APIView): notification = get_object_or_404(Notification, pk=notification_id, user=request.user) notification.last_read = read_at notification.save() + notification_read_event(request.user, notification) return Response({'message': _('Notification marked read.')}, status=status.HTTP_200_OK) app_name = request.data.get('app_name', '')