From b477a20ad2311a7c15f54ae6f85b8cb95cc1a4b0 Mon Sep 17 00:00:00 2001 From: Ahtisham Shahid Date: Thu, 6 Jul 2023 13:33:20 +0500 Subject: [PATCH] Added notifications for discussions events (#32432) * feat: added notifications for discussions app * feat: added unit tests for handler * feat: updated openedx-events package * fix: updated notification creation logic and tests * refactor: updated openedx-event version and event name * refactor: moved logic to separate methods --- .../student/models/course_enrollment.py | 4 +- lms/djangoapps/discussion/rest_api/api.py | 5 +- .../discussion/rest_api/tests/test_api.py | 8 +- .../discussion/rest_api/tests/test_utils.py | 111 +++++++++++++++++- .../discussion/rest_api/tests/utils.py | 16 +++ lms/djangoapps/discussion/rest_api/utils.py | 86 ++++++++++++++ .../core/djangoapps/notifications/admin.py | 2 +- .../notifications/base_notification.py | 1 - .../core/djangoapps/notifications/handlers.py | 34 ++++-- .../core/djangoapps/notifications/models.py | 12 +- .../djangoapps/notifications/serializers.py | 4 +- .../core/djangoapps/notifications/tasks.py | 28 +++++ .../tests/test_base_notification.py | 5 +- .../notifications/tests/test_views.py | 60 +++++++--- .../core/djangoapps/notifications/views.py | 2 +- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/kernel.in | 2 +- requirements/edx/testing.txt | 2 +- 19 files changed, 337 insertions(+), 49 deletions(-) diff --git a/common/djangoapps/student/models/course_enrollment.py b/common/djangoapps/student/models/course_enrollment.py index 227f60081e03..f44bd934ee57 100644 --- a/common/djangoapps/student/models/course_enrollment.py +++ b/common/djangoapps/student/models/course_enrollment.py @@ -74,6 +74,7 @@ class EnrollStatusChange: # complete a paid course purchase paid_complete = 'paid_complete' + UNENROLLED_TO_ALLOWEDTOENROLL = 'from unenrolled to allowed to enroll' ALLOWEDTOENROLL_TO_ENROLLED = 'from allowed to enroll to enrolled' ENROLLED_TO_ENROLLED = 'from enrolled to enrolled' @@ -95,7 +96,6 @@ class EnrollStatusChange: (DEFAULT_TRANSITION_STATE, DEFAULT_TRANSITION_STATE) ) - EVENT_NAME_ENROLLMENT_ACTIVATED = 'edx.course.enrollment.activated' EVENT_NAME_ENROLLMENT_DEACTIVATED = 'edx.course.enrollment.deactivated' EVENT_NAME_ENROLLMENT_MODE_CHANGED = 'edx.course.enrollment.mode_changed' @@ -292,7 +292,7 @@ def course_price(self): MODE_CACHE_NAMESPACE = 'CourseEnrollment.mode_and_active' class Meta: - unique_together = (('user', 'course'), ) + unique_together = (('user', 'course'),) indexes = [Index(fields=['user', '-created'])] ordering = ('user', 'course') diff --git a/lms/djangoapps/discussion/rest_api/api.py b/lms/djangoapps/discussion/rest_api/api.py index 7406676be738..edd5bd2da58c 100644 --- a/lms/djangoapps/discussion/rest_api/api.py +++ b/lms/djangoapps/discussion/rest_api/api.py @@ -128,7 +128,7 @@ discussion_open_for_user, get_usernames_for_course, get_usernames_from_search_string, - set_attribute + set_attribute, send_response_notifications ) @@ -1532,7 +1532,8 @@ def create_comment(request, comment_data): track_comment_created_event(request, course, cc_comment, cc_thread["commentable_id"], followed=False, from_mfe_sidebar=from_mfe_sidebar) - + send_response_notifications(thread=cc_thread, course=course, creator=request.user, + parent_id=comment_data.get("parent_id")) return api_comment diff --git a/lms/djangoapps/discussion/rest_api/tests/test_api.py b/lms/djangoapps/discussion/rest_api/tests/test_api.py index 7aa702a2d24c..3f7180cd28e2 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_api.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_api.py @@ -2243,13 +2243,16 @@ def test_success(self, parent_id, mock_emit): "/api/v1/threads/test_thread/comments" ) assert urlparse(httpretty.last_request().path).path == expected_url # lint-amnesty, pylint: disable=no-member - assert parsed_body(httpretty.last_request()) == { + + data = httpretty.latest_requests() + assert parsed_body(data[len(data) - 2]) == { 'course_id': [str(self.course.id)], 'body': ['Test body'], 'user_id': [str(self.user.id)], 'anonymous': ['False'], 'anonymous_to_peers': ['False'], } + expected_event_name = ( "edx.forum.comment.created" if parent_id else "edx.forum.response.created" @@ -2340,7 +2343,8 @@ def test_success_in_black_out_with_user_access(self, parent_id, mock_emit): "/api/v1/threads/test_thread/comments" ) assert urlparse(httpretty.last_request().path).path == expected_url # pylint: disable=no-member - assert parsed_body(httpretty.last_request()) == { + data = httpretty.latest_requests() + assert parsed_body(data[len(data) - 2]) == { "course_id": [str(self.course.id)], "body": ["Test body"], "user_id": [str(self.user.id)], diff --git a/lms/djangoapps/discussion/rest_api/tests/test_utils.py b/lms/djangoapps/discussion/rest_api/tests/test_utils.py index e64d36dfe42d..f55114702269 100644 --- a/lms/djangoapps/discussion/rest_api/tests/test_utils.py +++ b/lms/djangoapps/discussion/rest_api/tests/test_utils.py @@ -3,10 +3,14 @@ """ from datetime import datetime, timedelta +from unittest.mock import Mock +from httpretty import httpretty from pytz import UTC import unittest from common.djangoapps.student.roles import CourseStaffRole, CourseInstructorRole +from lms.djangoapps.discussion.django_comment_client.tests.utils import ForumsEnableMixin +from lms.djangoapps.discussion.rest_api.tests.utils import CommentsServiceMockMixin, ThreadMock from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -17,8 +21,11 @@ get_course_ta_users_list, get_course_staff_users_list, get_moderator_users_list, - get_archived_topics, remove_empty_sequentials + get_archived_topics, + remove_empty_sequentials, + send_response_notifications ) +from openedx_events.learning.signals import USER_NOTIFICATION_REQUESTED class DiscussionAPIUtilsTestCase(ModuleStoreTestCase): @@ -28,7 +35,7 @@ class DiscussionAPIUtilsTestCase(ModuleStoreTestCase): CREATE_USER = False def setUp(self): - super().setUp() # lint-amnesty, pylint: disable=super-with-arguments + super().setUp() # lint-amnesty, pylint: disable=super-with-arguments self.course = CourseFactory.create() self.course.discussion_blackouts = [datetime.now(UTC) - timedelta(days=3), @@ -100,6 +107,7 @@ class TestRemoveEmptySequentials(unittest.TestCase): """ Test for the remove_empty_sequentials function """ + def test_empty_data(self): # Test that the function can handle an empty list data = [] @@ -135,8 +143,7 @@ def test_remove_empty_sequentials(self): {"type": "chapter", "children": [ {"type": "sequential", "children": []}, {"type": "sequential", "children": []}, - ] - } + ]} ] expected_output = [ {"type": "chapter", "children": [ @@ -150,3 +157,99 @@ def test_remove_empty_sequentials(self): ] result = remove_empty_sequentials(data) self.assertEqual(result, expected_output) + + +class TestSendResponseNotifications(ForumsEnableMixin, CommentsServiceMockMixin, ModuleStoreTestCase): + """ + Test for the send_response_notifications function + """ + def setUp(self): + super().setUp() + httpretty.reset() + httpretty.enable() + + self.user_1 = UserFactory.create() + self.user_2 = UserFactory.create() + self.user_3 = UserFactory.create() + 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.course = CourseFactory.create() + + def test_send_notification_to_thread_creator(self): + """ + Test that the notification is sent to the thread creator + """ + handler = Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + + # Post the form or do what it takes to send the signal + + send_response_notifications(self.thread, self.course, self.user_2, parent_id=None) + self.assertEqual(handler.call_count, 1) + args = handler.call_args[1]['notification_data'] + self.assertEqual(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', + 'course_name': self.course.display_name, + } + self.assertDictEqual(args.context, expected_context) + self.assertEqual(args.content_url, 'http://example.com/1') + 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() + 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, self.course, self.user_3, 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(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', + 'course_name': self.course.display_name, + } + self.assertDictEqual(args_comment.context, expected_context) + self.assertEqual(args_comment.content_url, 'http://example.com/1') + self.assertEqual(args_comment.app_name, 'discussion') + + # check if the notification is sent to the parent response creator + self.assertEqual(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, + 'course_name': self.course.display_name, + } + self.assertDictEqual(args_comment_on_response.context, expected_context) + self.assertEqual(args_comment_on_response.content_url, 'http://example.com/1') + self.assertEqual(args_comment_on_response.app_name, 'discussion') + + def test_no_signal_on_creators_own_thread(self): + """ + Makes sure that no signal is emitted if user creates response on + their own thread. + """ + handler = Mock() + USER_NOTIFICATION_REQUESTED.connect(handler) + send_response_notifications(self.thread, self.course, self.user_1, parent_id=None) + self.assertEqual(handler.call_count, 0) diff --git a/lms/djangoapps/discussion/rest_api/tests/utils.py b/lms/djangoapps/discussion/rest_api/tests/utils.py index fd456674c3af..ee900876688b 100644 --- a/lms/djangoapps/discussion/rest_api/tests/utils.py +++ b/lms/djangoapps/discussion/rest_api/tests/utils.py @@ -656,3 +656,19 @@ def querystring(request): # This could just be HTTPrettyRequest.querystring, but that method double-decodes '%2B' -> '+' -> ' '. # You can just remove this method when this issue is fixed: https://github.com/gabrielfalcao/HTTPretty/issues/240 return parse_qs(request.path.split('?', 1)[-1]) + + +class ThreadMock(object): + """ + A mock thread object + """ + + def __init__(self, thread_id, creator, title, parent_id=None): + self.id = thread_id + self.user_id = creator.id + self.username = creator.username + self.title = title + self.parent_id = parent_id + + def url_with_id(self, params): + return f"http://example.com/{params['id']}" diff --git a/lms/djangoapps/discussion/rest_api/utils.py b/lms/djangoapps/discussion/rest_api/utils.py index 06d726346b23..c869f1ef3984 100644 --- a/lms/djangoapps/discussion/rest_api/utils.py +++ b/lms/djangoapps/discussion/rest_api/utils.py @@ -16,6 +16,9 @@ FORUM_ROLE_GROUP_MODERATOR, FORUM_ROLE_COMMUNITY_TA, ) +from openedx_events.learning.signals import USER_NOTIFICATION_REQUESTED +from openedx_events.learning.data import UserNotificationData +from openedx.core.djangoapps.django_comment_common.comment_client.comment import Comment class AttributeDict(dict): @@ -351,3 +354,86 @@ def get_archived_topics(filtered_topic_ids: List[str], topics: List[Dict[str, st if topic['id'] == topic_id and topic['usage_key'] is not None: archived_topics.append(topic) return archived_topics + + +def send_response_notifications(thread, course, creator, parent_id=None): + """ + Send notifications to users who are subscribed to the thread. + """ + notification_sender = DiscussionNotificationSender(thread, course, creator, parent_id) + notification_sender.send_new_comment_notification() + notification_sender.send_new_response_notification() + notification_sender.send_new_comment_on_response_notification() + + +class DiscussionNotificationSender: + """ + Class to send notifications to users who are subscribed to the thread. + """ + + def __init__(self, thread, course, creator, parent_id=None): + self.thread = thread + self.course = course + self.creator = creator + self.parent_id = parent_id + self.parent_response = None + self._get_parent_response() + + def _send_notification(self, user_ids, notification_type, extra_context=None): + """ + Send notification to users + """ + if not user_ids: + return + + if extra_context is None: + extra_context = {} + + notification_data = UserNotificationData( + user_ids=user_ids, + context={ + "replier_name": self.creator.username, + "post_title": self.thread.title, + "course_name": self.course.display_name, + **extra_context, + }, + notification_type=notification_type, + content_url=self.thread.url_with_id(params={'id': self.thread.id}), + app_name="discussion", + course_key=self.course.id, + ) + USER_NOTIFICATION_REQUESTED.send_event(notification_data=notification_data) + + def _get_parent_response(self): + """ + Get parent response object + """ + if self.parent_id and not self.parent_response: + self.parent_response = Comment(id=self.parent_id).retrieve() + + return self.parent_response + + def send_new_response_notification(self): + """ + Send notification to users who are subscribed to the main thread/post i.e. + there is a response to the main thread. + """ + if not self.parent_id and self.creator.id != self.thread.user_id: + self._send_notification([self.thread.user_id], "new_response") + + def send_new_comment_notification(self): + """ + Send notification to parent thread creator i.e. comment on the response. + """ + if self.parent_response and self.creator.id != int(self.thread.user_id): + context = { + "author_name": self.parent_response.username, + } + self._send_notification([self.thread.user_id], "new_comment", extra_context=context) + + def send_new_comment_on_response_notification(self): + """ + Send notification to parent response creator i.e. comment on the response. + """ + if self.parent_response and self.creator.id != int(self.parent_response.user_id): + self._send_notification([self.parent_response.user_id], "new_comment_on_response") diff --git a/openedx/core/djangoapps/notifications/admin.py b/openedx/core/djangoapps/notifications/admin.py index 2a7f13e60b34..65b7db61fd17 100644 --- a/openedx/core/djangoapps/notifications/admin.py +++ b/openedx/core/djangoapps/notifications/admin.py @@ -4,7 +4,7 @@ from django.contrib import admin -from .models import Notification, CourseNotificationPreference +from .models import CourseNotificationPreference, Notification class NotificationAdmin(admin.ModelAdmin): diff --git a/openedx/core/djangoapps/notifications/base_notification.py b/openedx/core/djangoapps/notifications/base_notification.py index 742a76b93b39..01820c3b5089 100644 --- a/openedx/core/djangoapps/notifications/base_notification.py +++ b/openedx/core/djangoapps/notifications/base_notification.py @@ -8,7 +8,6 @@ find_pref_in_normalized_prefs, ) - COURSE_NOTIFICATION_TYPES = { 'new_comment_on_response': { 'notification_app': 'discussion', diff --git a/openedx/core/djangoapps/notifications/handlers.py b/openedx/core/djangoapps/notifications/handlers.py index e3f2307f0f5a..36dc91ca9e74 100644 --- a/openedx/core/djangoapps/notifications/handlers.py +++ b/openedx/core/djangoapps/notifications/handlers.py @@ -5,29 +5,34 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import IntegrityError -from django.db.models.signals import post_save from django.dispatch import receiver -from openedx_events.learning.signals import COURSE_UNENROLLMENT_COMPLETED +from openedx_events.learning.signals import ( + COURSE_ENROLLMENT_CREATED, + COURSE_UNENROLLMENT_COMPLETED, + USER_NOTIFICATION_REQUESTED +) from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS from openedx.core.djangoapps.notifications.models import CourseNotificationPreference - log = logging.getLogger(__name__) -@receiver(post_save, sender='student.CourseEnrollment') -def course_enrollment_post_save(sender, instance, created, **kwargs): +@receiver(COURSE_ENROLLMENT_CREATED) +def course_enrollment_post_save(signal, sender, enrollment, metadata, **kwargs): """ Watches for post_save signal for creates on the CourseEnrollment table. Generate a CourseNotificationPreference if new Enrollment is created """ - if created and ENABLE_NOTIFICATIONS.is_enabled(instance.course_id): + if ENABLE_NOTIFICATIONS.is_enabled(enrollment.course.course_key): try: - CourseNotificationPreference.objects.create(user=instance.user, course_id=instance.course_id) + CourseNotificationPreference.objects.create( + user_id=enrollment.user.id, + course_id=enrollment.course.course_key + ) except IntegrityError: - log.info(f'CourseNotificationPreference already exists for user {instance.user} ' - f'and course {instance.course_id}') + log.info(f'CourseNotificationPreference already exists for user {enrollment.user} ' + f'and course {enrollment.course_id}') @receiver(COURSE_UNENROLLMENT_COMPLETED) @@ -42,3 +47,14 @@ def on_user_course_unenrollment(enrollment, **kwargs): preference.delete() except ObjectDoesNotExist: log.info(f'Notification Preference doesnot exist for {enrollment.user.pii.username} in {course_key}') + + +@receiver(USER_NOTIFICATION_REQUESTED) +def generate_user_notifications(signal, sender, notification_data, metadata, **kwargs): + """ + Watches for USER_NOTIFICATION_REQUESTED signal and calls send_web_notifications task + """ + from openedx.core.djangoapps.notifications.tasks import send_notifications + notification_data = notification_data.__dict__ + notification_data['course_key'] = str(notification_data['course_key']) + send_notifications.delay(**notification_data) diff --git a/openedx/core/djangoapps/notifications/models.py b/openedx/core/djangoapps/notifications/models.py index 27b0138301c4..e651c5e8bbf7 100644 --- a/openedx/core/djangoapps/notifications/models.py +++ b/openedx/core/djangoapps/notifications/models.py @@ -11,10 +11,9 @@ from openedx.core.djangoapps.notifications.base_notification import ( NotificationAppManager, NotificationPreferenceSyncManager, - get_notification_content, + get_notification_content ) - User = get_user_model() log = logging.getLogger(__name__) @@ -151,3 +150,12 @@ def get_updated_user_course_preferences(user, course_id): except Exception as e: log.error(f'Unable to update notification preference for {user.username} to new config. {e}') return preferences + + def get_app_config(self, app_name) -> dict: + return self.notification_preference_config.get(app_name, {}) + + def get_notification_type_config(self, app_name, notification_type) -> dict: + return self.get_app_config(app_name).get(notification_type, {}) + + def get_web_config(self, app_name, notification_type) -> bool: + return self.get_notification_type_config(app_name, notification_type).get('web', False) diff --git a/openedx/core/djangoapps/notifications/serializers.py b/openedx/core/djangoapps/notifications/serializers.py index 2f3023d841f8..eb707bcb8dba 100644 --- a/openedx/core/djangoapps/notifications/serializers.py +++ b/openedx/core/djangoapps/notifications/serializers.py @@ -7,9 +7,9 @@ from common.djangoapps.student.models import CourseEnrollment from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.notifications.models import ( - get_notification_channels, - Notification, CourseNotificationPreference, + Notification, + get_notification_channels ) diff --git a/openedx/core/djangoapps/notifications/tasks.py b/openedx/core/djangoapps/notifications/tasks.py index 38fc8c424f85..ed681a4dee26 100644 --- a/openedx/core/djangoapps/notifications/tasks.py +++ b/openedx/core/djangoapps/notifications/tasks.py @@ -70,3 +70,31 @@ def delete_expired_notifications(): logger.info(f'{delete_count} Notifications deleted in current batch in {time_elapsed} seconds.') time_elapsed = datetime.now() - start_time logger.info(f'{total_deleted} Notifications deleted in {time_elapsed} seconds.') + + +@shared_task +@set_code_owner_attribute +def send_notifications(user_ids, course_key, app_name, notification_type, context, content_url): + """ + Send notifications to the users. + """ + user_ids = list(set(user_ids)) + + # check if what is preferences of user and make decision to send notification or not + preferences = CourseNotificationPreference.objects.filter( + user_id__in=user_ids, + course_id=course_key, + ) + notifications = [] + for preference in preferences: + if preference and preference.get_web_config(app_name, notification_type): + notifications.append(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, + )) + # send notification to users but use bulk_create + Notification.objects.bulk_create(notifications) diff --git a/openedx/core/djangoapps/notifications/tests/test_base_notification.py b/openedx/core/djangoapps/notifications/tests/test_base_notification.py index 6248575d164f..e37fbe7155d7 100644 --- a/openedx/core/djangoapps/notifications/tests/test_base_notification.py +++ b/openedx/core/djangoapps/notifications/tests/test_base_notification.py @@ -2,11 +2,10 @@ Tests for base_notification """ from common.djangoapps.student.tests.factories import UserFactory -from openedx.core.djangoapps.notifications import base_notification -from openedx.core.djangoapps.notifications import models +from openedx.core.djangoapps.notifications import base_notification, models from openedx.core.djangoapps.notifications.models import ( CourseNotificationPreference, - get_course_notification_preference_config_version, + get_course_notification_preference_config_version ) from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index 8bce8898d026..3a977ea6ce27 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -6,9 +6,10 @@ import ddt from django.conf import settings -from django.dispatch import Signal from django.urls import reverse from edx_toggles.toggles.testutils import override_waffle_flag +from openedx_events.learning.data import CourseData, CourseEnrollmentData, UserData, UserPersonalData +from openedx_events.learning.signals import COURSE_ENROLLMENT_CREATED from pytz import UTC from rest_framework import status from rest_framework.test import APIClient, APITestCase @@ -17,10 +18,7 @@ from common.djangoapps.student.tests.factories import UserFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS, SHOW_NOTIFICATIONS_TRAY -from openedx.core.djangoapps.notifications.models import ( - Notification, - CourseNotificationPreference, -) +from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification from openedx.core.djangoapps.notifications.serializers import NotificationCourseEnrollmentSerializer from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory @@ -118,18 +116,32 @@ def setUp(self): is_active=True, mode='audit' ) - self.post_save_signal = Signal() def test_course_enrollment_post_save(self): """ Test the post_save signal for CourseEnrollment. """ # Emit post_save signal - - self.post_save_signal.send( - sender=self.course_enrollment.__class__, - instance=self.course_enrollment, - created=True + enrollment_data = CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course.id, + display_name=self.course.display_name, + ), + mode=self.course_enrollment.mode, + is_active=self.course_enrollment.is_active, + creation_date=self.course_enrollment.created, + ) + COURSE_ENROLLMENT_CREATED.send_event( + enrollment=enrollment_data ) # Assert that CourseNotificationPreference object was created with correct attributes @@ -162,13 +174,29 @@ def setUp(self): is_active=True, mode='audit' ) - self.post_save_signal = Signal() self.client = APIClient() self.path = reverse('notification-preferences', kwargs={'course_key_string': self.course.id}) - self.post_save_signal.send( - sender=self.course_enrollment.__class__, - instance=self.course_enrollment, - created=True + + enrollment_data = CourseEnrollmentData( + user=UserData( + pii=UserPersonalData( + username=self.user.username, + email=self.user.email, + name=self.user.profile.name, + ), + id=self.user.id, + is_active=self.user.is_active, + ), + course=CourseData( + course_key=self.course.id, + display_name=self.course.display_name, + ), + mode=self.course_enrollment.mode, + is_active=self.course_enrollment.is_active, + creation_date=self.course_enrollment.created, + ) + COURSE_ENROLLMENT_CREATED.send_event( + enrollment=enrollment_data ) def _expected_api_response(self): diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py index f0836499897e..2ea38d5e8a27 100644 --- a/openedx/core/djangoapps/notifications/views.py +++ b/openedx/core/djangoapps/notifications/views.py @@ -17,7 +17,7 @@ from common.djangoapps.student.models import CourseEnrollment from openedx.core.djangoapps.notifications.models import ( CourseNotificationPreference, - get_course_notification_preference_config_version, + get_course_notification_preference_config_version ) from .base_notification import COURSE_NOTIFICATION_APPS diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 82da2d3df319..2f283b05fb10 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -769,7 +769,7 @@ openedx-django-require==2.0.0 # via -r requirements/edx/kernel.in openedx-django-wiki==2.0.0 # via -r requirements/edx/kernel.in -openedx-events==8.0.1 +openedx-events==8.2.0 # via # -r requirements/edx/kernel.in # edx-event-bus-kafka diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index add14c14aa14..d2df91c43d67 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -1033,7 +1033,7 @@ openedx-django-require==2.0.0 # via -r requirements/edx/testing.txt openedx-django-wiki==2.0.0 # via -r requirements/edx/testing.txt -openedx-events==8.0.1 +openedx-events==8.2.0 # via # -r requirements/edx/testing.txt # edx-event-bus-kafka diff --git a/requirements/edx/kernel.in b/requirements/edx/kernel.in index 6a4cd5bc7675..43395d9f7efe 100644 --- a/requirements/edx/kernel.in +++ b/requirements/edx/kernel.in @@ -114,7 +114,7 @@ olxcleaner openedx-calc # Library supporting mathematical calculations for Open edX openedx-django-require # openedx-events 3.1.0 introduces producer API -openedx-events>=3.1.0 # Open edX Events from Hooks Extension Framework (OEP-50) +openedx-events>=8.2.0 # Open edX Events from Hooks Extension Framework (OEP-50) openedx-filters # Open edX Filters from Hooks Extension Framework (OEP-50) openedx-mongodbproxy openedx-django-wiki diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt index d72610756eaa..c7a565313a8a 100644 --- a/requirements/edx/testing.txt +++ b/requirements/edx/testing.txt @@ -978,7 +978,7 @@ openedx-django-require==2.0.0 # via -r requirements/edx/base.txt openedx-django-wiki==2.0.0 # via -r requirements/edx/base.txt -openedx-events==8.0.1 +openedx-events==8.2.0 # via # -r requirements/edx/base.txt # edx-event-bus-kafka