From 692caf0f46d62e4f65edb762bb81751980c5314a Mon Sep 17 00:00:00 2001 From: jawad khan Date: Mon, 21 Jul 2025 14:01:11 +0500 Subject: [PATCH] feat: Added audit access expiry soon notification (#36414) * feat: Added audit access expiry soon notification --- .../enrollments/enrollments_notifications.py | 36 ++++++++++ .../tests/test_enrollments_notifications.py | 49 ++++++++++++++ .../notifications/base_notification.py | 29 +++++++++ .../core/djangoapps/notifications/models.py | 2 +- .../djangoapps/notifications/push/tasks.py | 1 + .../notifications/tests/test_views.py | 65 ++++++++++++++++++- 6 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 openedx/core/djangoapps/enrollments/enrollments_notifications.py create mode 100644 openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py diff --git a/openedx/core/djangoapps/enrollments/enrollments_notifications.py b/openedx/core/djangoapps/enrollments/enrollments_notifications.py new file mode 100644 index 0000000000..90f8a42d97 --- /dev/null +++ b/openedx/core/djangoapps/enrollments/enrollments_notifications.py @@ -0,0 +1,36 @@ +""" +Enrollment notifications sender util. +""" +from django.conf import settings + +from openedx_events.learning.data import UserNotificationData +from openedx_events.learning.signals import USER_NOTIFICATION_REQUESTED + + +class EnrollmentNotificationSender: + """ + Class to send notifications to user about their enrollments. + """ + + def __init__(self, course, user_id, audit_access_expiry): + self.course = course + self.user_id = user_id + self.audit_access_expiry = audit_access_expiry + + def send_audit_access_expiring_soon_notification(self): + """ + Send audit access expiring soon notification to user + """ + + notification_data = UserNotificationData( + user_ids=[int(self.user_id)], + context={ + 'course': self.course.name, + 'audit_access_expiry': self.audit_access_expiry, + }, + notification_type='audit_access_expiring_soon', + content_url=f"{settings.LEARNING_MICROFRONTEND_URL}/course/{str(self.course.id)}/home", + app_name="enrollments", + course_key=self.course.id, + ) + USER_NOTIFICATION_REQUESTED.send_event(notification_data=notification_data) diff --git a/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py b/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py new file mode 100644 index 0000000000..5420733107 --- /dev/null +++ b/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py @@ -0,0 +1,49 @@ +""" +Unit tests for the EnrollmentsNotificationSender class +""" +import unittest +import datetime +from unittest.mock import MagicMock, patch + +from django.conf import settings +import pytest + +from openedx.core.djangoapps.enrollments.enrollments_notifications import EnrollmentNotificationSender +from openedx_events.learning.data import UserNotificationData + + +@pytest.mark.django_db +class TestEnrollmentsNotificationSender(unittest.TestCase): + """ + Tests for the EnrollmentsNotificationSender class + """ + + def setUp(self): + self.course = MagicMock() + self.course.name = "test course" + self.course.id = 1 + self.expiry_date = datetime.date.today() + datetime.timedelta(days=1) + self.user_id = '123' + self.notification_sender = EnrollmentNotificationSender(self.course, self.user_id, self.expiry_date) + + @patch('openedx.core.djangoapps.enrollments.enrollments_notifications.USER_NOTIFICATION_REQUESTED.send_event') + def test_send_audit_access_expiring_soon_notification(self, mock_send_notification): + """ + Test that audit access expiring soon notification event is sent with correct parameters. + """ + + self.notification_sender.send_audit_access_expiring_soon_notification() + + mock_send_notification.assert_called_once() + notification_data = UserNotificationData( + user_ids=[int(self.user_id)], + context={ + 'course': self.course.name, + 'audit_access_expiry': self.expiry_date, + }, + notification_type='audit_access_expiring_soon', + content_url=f"{settings.LEARNING_MICROFRONTEND_URL}/course/{str(self.course.id)}/home", + app_name="enrollments", + course_key=self.course.id, + ) + mock_send_notification.assert_called_with(notification_data=notification_data) diff --git a/openedx/core/djangoapps/notifications/base_notification.py b/openedx/core/djangoapps/notifications/base_notification.py index 5264053ace..0592e4aff5 100644 --- a/openedx/core/djangoapps/notifications/base_notification.py +++ b/openedx/core/djangoapps/notifications/base_notification.py @@ -248,6 +248,26 @@ COURSE_NOTIFICATION_TYPES = { 'email_template': '', 'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE] }, + 'audit_access_expiring_soon': { + 'notification_app': 'enrollments', + 'name': 'audit_access_expiring_soon', + 'is_core': False, + 'info': '', + 'web': True, + 'email': False, + 'email_cadence': EmailCadence.DAILY, + 'push': False, + 'non_editable': [], + 'content_template': _('<{p}>Your audit access for <{strong}>{course_name} is expiring on ' + '<{strong}>{audit_access_expiry}. ' + 'Upgrade now to extend access and get a certificate!.'), + 'content_context': { + 'course_name': 'Course name', + 'audit_access_expiry': 'Audit access expiry date', + }, + 'email_template': '', + 'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE], + }, } COURSE_NOTIFICATION_APPS = { @@ -279,6 +299,15 @@ COURSE_NOTIFICATION_APPS = { 'core_email_cadence': EmailCadence.DAILY, 'non_editable': [] }, + 'enrollments': { + 'enabled': True, + 'core_info': _('Notifications for enrollments.'), + 'core_web': True, + 'core_email': True, + 'core_push': True, + 'core_email_cadence': EmailCadence.DAILY, + 'non_editable': [] + } } diff --git a/openedx/core/djangoapps/notifications/models.py b/openedx/core/djangoapps/notifications/models.py index 2e6f9c13c9..a555571e9f 100644 --- a/openedx/core/djangoapps/notifications/models.py +++ b/openedx/core/djangoapps/notifications/models.py @@ -26,7 +26,7 @@ NOTIFICATION_CHANNELS = ['web', 'push', 'email'] ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS = ['email_cadence'] # Update this version when there is a change to any course specific notification type or app. -COURSE_NOTIFICATION_CONFIG_VERSION = 14 +COURSE_NOTIFICATION_CONFIG_VERSION = 15 def get_course_notification_preference_config(): diff --git a/openedx/core/djangoapps/notifications/push/tasks.py b/openedx/core/djangoapps/notifications/push/tasks.py index 9a9e225cba..c9193a8a7f 100644 --- a/openedx/core/djangoapps/notifications/push/tasks.py +++ b/openedx/core/djangoapps/notifications/push/tasks.py @@ -25,6 +25,7 @@ def send_ace_msg_to_push_channel(audience_ids, notification_object): notification_type = notification_object.notification_type post_data = { + 'notification_id': notification_object.id, 'notification_type': notification_type, 'course_id': str(notification_object.course_id), 'content_url': notification_object.content_url, diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py index 9d5e3cdaab..ff962db448 100644 --- a/openedx/core/djangoapps/notifications/tests/test_views.py +++ b/openedx/core/djangoapps/notifications/tests/test_views.py @@ -399,6 +399,27 @@ class UserNotificationPreferenceAPITest(ModuleStoreTestCase): 'non_editable': { 'ora_grade_assigned': ['push'] } + }, + "enrollments": { + "enabled": True, + "core_notification_types": [], + "notification_types": { + "audit_access_expiring_soon": { + "web": True, + "email": False, + "push": False, + "email_cadence": "Daily", + "info": "" + }, + "core": { + "web": True, + "email": True, + "push": True, + "email_cadence": "Daily", + "info": "Notifications for enrollments." + } + }, + "non_editable": {} } } } @@ -824,7 +845,8 @@ class NotificationCountViewSetTestCase(ModuleStoreTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.data['count'], 4) self.assertEqual(response.data['count_by_app_name'], { - 'App Name 1': 2, 'App Name 2': 1, 'App Name 3': 1, 'discussion': 0, 'updates': 0, 'grading': 0}) + 'App Name 1': 2, 'App Name 2': 1, 'App Name 3': 1, 'discussion': 0, + 'updates': 0, 'grading': 0, 'enrollments': 0}) self.assertEqual(response.data['show_notifications_tray'], True) def test_get_unseen_notifications_count_for_unauthenticated_user(self): @@ -845,7 +867,8 @@ class NotificationCountViewSetTestCase(ModuleStoreTestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.data['count'], 0) - self.assertEqual(response.data['count_by_app_name'], {'discussion': 0, 'updates': 0, 'grading': 0}) + self.assertEqual(response.data['count_by_app_name'], {'discussion': 0, 'updates': 0, + 'grading': 0, 'enrollments': 0}) def test_get_expiry_days_in_count_view(self): """ @@ -1539,6 +1562,25 @@ class TestNotificationPreferencesView(APITestCase): "ora_grade_assigned": ["push"], "ora_staff_notifications": ["push"] } + }, + "enrollments": { + "enabled": True, + "core_notification_types": [], + "notification_types": { + "audit_access_expiring_soon": { + "web": True, + "email": False, + "push": False, + "email_cadence": "Daily" + }, + "core": { + "web": True, + "email": True, + "push": True, + "email_cadence": "Daily" + } + }, + "non_editable": {} } } } @@ -1674,6 +1716,25 @@ class TestNotificationPreferencesView(APITestCase): "ora_grade_assigned": ["push"], "ora_staff_notifications": ["push"] } + }, + "enrollments": { + "enabled": True, + "core_notification_types": [], + "notification_types": { + "audit_access_expiring_soon": { + "web": False, + "email": False, + "push": False, + "email_cadence": "Daily" + }, + "core": { + "web": True, + "email": True, + "push": True, + "email_cadence": "Daily" + } + }, + "non_editable": {} } } }