From 98dfb12943faf1f9762824cfb3702d0cbd583a3a Mon Sep 17 00:00:00 2001
From: Muhammad Adeel Tajamul
<77053848+muhammadadeeltajamul@users.noreply.github.com>
Date: Fri, 28 Jun 2024 11:22:09 +0500
Subject: [PATCH] feat: added unsubsribe url for email notifications (#34967)
---
.../djangoapps/notifications/email/tasks.py | 4 +-
.../notifications/email/tests/test_utils.py | 243 +++++++++++++++++-
.../djangoapps/notifications/email/utils.py | 129 +++++++++-
.../notifications/digest_footer.html | 3 +
.../notifications/tests/test_views.py | 37 +++
openedx/core/djangoapps/notifications/urls.py | 7 +-
.../core/djangoapps/notifications/views.py | 13 +
7 files changed, 426 insertions(+), 10 deletions(-)
diff --git a/openedx/core/djangoapps/notifications/email/tasks.py b/openedx/core/djangoapps/notifications/email/tasks.py
index 3b1783f42b3a..50e8455af715 100644
--- a/openedx/core/djangoapps/notifications/email/tasks.py
+++ b/openedx/core/djangoapps/notifications/email/tasks.py
@@ -92,8 +92,8 @@ def send_digest_email_to_user(user, cadence_type, course_language='en', courses_
logger.info(f' No filtered notification for {user.username} ==Temp Log==')
return
apps_dict = create_app_notifications_dict(notifications)
- message_context = create_email_digest_context(apps_dict, start_date, end_date, cadence_type,
- courses_data=courses_data)
+ message_context = create_email_digest_context(apps_dict, user.username, start_date, end_date,
+ cadence_type, courses_data=courses_data)
recipient = Recipient(user.id, user.email)
message = EmailNotificationMessageType(
app_label="notifications", name="email_digest"
diff --git a/openedx/core/djangoapps/notifications/email/tests/test_utils.py b/openedx/core/djangoapps/notifications/email/tests/test_utils.py
index d7c9f6c98133..ee95d7af3991 100644
--- a/openedx/core/djangoapps/notifications/email/tests/test_utils.py
+++ b/openedx/core/djangoapps/notifications/email/tests/test_utils.py
@@ -4,21 +4,32 @@
import datetime
import ddt
+from itertools import product
from pytz import utc
from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
from common.djangoapps.student.tests.factories import UserFactory
+from openedx.core.djangoapps.notifications.base_notification import (
+ COURSE_NOTIFICATION_APPS,
+ COURSE_NOTIFICATION_TYPES,
+)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
-from openedx.core.djangoapps.notifications.models import Notification
+from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
+from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
from openedx.core.djangoapps.notifications.email.utils import (
add_additional_attributes_to_notifications,
create_app_notifications_dict,
create_datetime_string,
create_email_digest_context,
create_email_template_context,
+ decrypt_object,
+ decrypt_string,
+ encrypt_object,
+ encrypt_string,
get_course_info,
get_time_ago,
is_email_notification_flag_enabled,
+ update_user_preferences_from_patch,
)
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -102,8 +113,9 @@ def test_email_template_context(self):
"""
Tests common header and footer context
"""
- context = create_email_template_context()
- keys = ['platform_name', 'mailing_address', 'logo_url', 'social_media', 'notification_settings_url']
+ context = create_email_template_context(self.user.username)
+ keys = ['platform_name', 'mailing_address', 'logo_url', 'social_media',
+ 'notification_settings_url', 'unsubscribe_url']
for key in keys:
assert key in context
@@ -121,6 +133,7 @@ def test_email_digest_context(self, digest_frequency):
end_date = datetime.datetime(2024, 3, 24, 12, 0)
params = {
"app_notifications_dict": app_dict,
+ "username": self.user.username,
"start_date": end_date - datetime.timedelta(days=0 if digest_frequency == "Daily" else 6),
"end_date": end_date,
"digest_frequency": digest_frequency,
@@ -194,3 +207,227 @@ def test_waffle_flag_everyone_priority(self):
assert is_email_notification_flag_enabled() is False
assert is_email_notification_flag_enabled(self.user_1) is False
assert is_email_notification_flag_enabled(self.user_2) is False
+
+
+class TestEncryption(ModuleStoreTestCase):
+ """
+ Tests all encryption methods
+ """
+ def test_string_encryption(self):
+ """
+ Tests if decrypted string is equal original string
+ """
+ string = "edx"
+ encrypted = encrypt_string(string)
+ decrypted = decrypt_string(encrypted)
+ assert string == decrypted
+
+ def test_object_encryption(self):
+ """
+ Tests if decrypted object is equal to original object
+ """
+ obj = {
+ 'org': 'edx'
+ }
+ encrypted = encrypt_object(obj)
+ decrypted = decrypt_object(encrypted)
+ assert obj == decrypted
+
+
+@ddt.ddt
+class TestUpdatePreferenceFromPatch(ModuleStoreTestCase):
+ """
+ Tests if preferences are update according to patch data
+ """
+ def setUp(self):
+ """
+ Setup test cases
+ """
+ super().setUp()
+ self.user = UserFactory()
+ self.course_1 = CourseFactory.create(display_name='test course 1', run="Testing_course_1")
+ self.course_2 = CourseFactory.create(display_name='test course 2', run="Testing_course_2")
+ self.preference_1 = CourseNotificationPreference(course_id=self.course_1.id, user=self.user)
+ self.preference_2 = CourseNotificationPreference(course_id=self.course_2.id, user=self.user)
+ self.preference_1.save()
+ self.preference_2.save()
+ self.default_json = self.preference_1.notification_preference_config
+
+ def is_channel_editable(self, app_name, notification_type, channel):
+ """
+ Returns if channel is editable
+ """
+ if notification_type == 'core':
+ return channel not in COURSE_NOTIFICATION_APPS[app_name]['non_editable']
+ return channel not in COURSE_NOTIFICATION_TYPES[notification_type]['non_editable']
+
+ def get_default_cadence_value(self, app_name, notification_type):
+ """
+ Returns default email cadence value
+ """
+ if notification_type == 'core':
+ return COURSE_NOTIFICATION_APPS[app_name]['core_email_cadence']
+ return COURSE_NOTIFICATION_TYPES[notification_type]['email_cadence']
+
+ @ddt.data(True, False)
+ def test_value_param(self, new_value):
+ """
+ Tests if value is updated for all notification types and for all channels
+ """
+ encrypted_username = encrypt_string(self.user.username)
+ encrypted_patch = encrypt_object({
+ 'value': new_value
+ })
+ update_user_preferences_from_patch(encrypted_username, encrypted_patch)
+ preference_1 = CourseNotificationPreference.objects.get(course_id=self.course_1.id, user=self.user)
+ preference_2 = CourseNotificationPreference.objects.get(course_id=self.course_2.id, user=self.user)
+ for preference in [preference_1, preference_2]:
+ config = preference.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for noti_type, type_prefs in app_prefs['notification_types'].items():
+ for channel in ['web', 'email', 'push']:
+ if self.is_channel_editable(app_name, noti_type, channel):
+ assert type_prefs[channel] == new_value
+ else:
+ default_app_json = self.default_json[app_name]
+ default_notification_type_json = default_app_json['notification_types'][noti_type]
+ assert type_prefs[channel] == default_notification_type_json[channel]
+
+ @ddt.data(*product(['web', 'email', 'push'], [True, False]))
+ @ddt.unpack
+ def test_value_with_channel_param(self, param_channel, new_value):
+ """
+ Tests if value is updated only for channel
+ """
+ encrypted_username = encrypt_string(self.user.username)
+ encrypted_patch = encrypt_object({
+ 'channel': param_channel,
+ 'value': new_value
+ })
+ update_user_preferences_from_patch(encrypted_username, encrypted_patch)
+ preference_1 = CourseNotificationPreference.objects.get(course_id=self.course_1.id, user=self.user)
+ preference_2 = CourseNotificationPreference.objects.get(course_id=self.course_2.id, user=self.user)
+ # pylint: disable=too-many-nested-blocks
+ for preference in [preference_1, preference_2]:
+ config = preference.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for noti_type, type_prefs in app_prefs['notification_types'].items():
+ for channel in ['web', 'email', 'push']:
+ if not self.is_channel_editable(app_name, noti_type, channel):
+ continue
+ if channel == param_channel:
+ assert type_prefs[channel] == new_value
+ if channel == 'email':
+ cadence_value = EmailCadence.NEVER
+ if new_value:
+ cadence_value = self.get_default_cadence_value(app_name, noti_type)
+ assert type_prefs['email_cadence'] == cadence_value
+ else:
+ default_app_json = self.default_json[app_name]
+ default_notification_type_json = default_app_json['notification_types'][noti_type]
+ assert type_prefs[channel] == default_notification_type_json[channel]
+
+ @ddt.data(True, False)
+ def test_value_with_course_id_param(self, new_value):
+ """
+ Tests if value is updated for a single course only
+ """
+ encrypted_username = encrypt_string(self.user.username)
+ encrypted_patch = encrypt_object({
+ 'value': new_value,
+ 'course_id': str(self.course_1.id),
+ })
+ update_user_preferences_from_patch(encrypted_username, encrypted_patch)
+
+ preference_2 = CourseNotificationPreference.objects.get(course_id=self.course_2.id, user=self.user)
+ self.assertDictEqual(preference_2.notification_preference_config, self.default_json)
+
+ preference_1 = CourseNotificationPreference.objects.get(course_id=self.course_1.id, user=self.user)
+ config = preference_1.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for noti_type, type_prefs in app_prefs['notification_types'].items():
+ for channel in ['web', 'email', 'push']:
+ if self.is_channel_editable(app_name, noti_type, channel):
+ assert type_prefs[channel] == new_value
+ else:
+ default_app_json = self.default_json[app_name]
+ default_notification_type_json = default_app_json['notification_types'][noti_type]
+ assert type_prefs[channel] == default_notification_type_json[channel]
+
+ @ddt.data(*product(['discussion', 'updates'], [True, False]))
+ @ddt.unpack
+ def test_value_with_app_name_param(self, param_app_name, new_value):
+ """
+ Tests if value is updated only for channel
+ """
+ encrypted_username = encrypt_string(self.user.username)
+ encrypted_patch = encrypt_object({
+ 'app_name': param_app_name,
+ 'value': new_value
+ })
+ update_user_preferences_from_patch(encrypted_username, encrypted_patch)
+ preference_1 = CourseNotificationPreference.objects.get(course_id=self.course_1.id, user=self.user)
+ preference_2 = CourseNotificationPreference.objects.get(course_id=self.course_2.id, user=self.user)
+ # pylint: disable=too-many-nested-blocks
+ for preference in [preference_1, preference_2]:
+ config = preference.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for noti_type, type_prefs in app_prefs['notification_types'].items():
+ for channel in ['web', 'email', 'push']:
+ if not self.is_channel_editable(app_name, noti_type, channel):
+ continue
+ if app_name == param_app_name:
+ assert type_prefs[channel] == new_value
+ if channel == 'email':
+ cadence_value = EmailCadence.NEVER
+ if new_value:
+ cadence_value = self.get_default_cadence_value(app_name, noti_type)
+ assert type_prefs['email_cadence'] == cadence_value
+ else:
+ default_app_json = self.default_json[app_name]
+ default_notification_type_json = default_app_json['notification_types'][noti_type]
+ assert type_prefs[channel] == default_notification_type_json[channel]
+
+ @ddt.data(*product(['new_discussion_post', 'content_reported'], [True, False]))
+ @ddt.unpack
+ def test_value_with_notification_type_param(self, param_notification_type, new_value):
+ """
+ Tests if value is updated only for channel
+ """
+ encrypted_username = encrypt_string(self.user.username)
+ encrypted_patch = encrypt_object({
+ 'notification_type': param_notification_type,
+ 'value': new_value
+ })
+ update_user_preferences_from_patch(encrypted_username, encrypted_patch)
+ preference_1 = CourseNotificationPreference.objects.get(course_id=self.course_1.id, user=self.user)
+ preference_2 = CourseNotificationPreference.objects.get(course_id=self.course_2.id, user=self.user)
+ # pylint: disable=too-many-nested-blocks
+ for preference in [preference_1, preference_2]:
+ config = preference.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for noti_type, type_prefs in app_prefs['notification_types'].items():
+ for channel in ['web', 'email', 'push']:
+ if not self.is_channel_editable(app_name, noti_type, channel):
+ continue
+ if noti_type == param_notification_type:
+ assert type_prefs[channel] == new_value
+ if channel == 'email':
+ cadence_value = EmailCadence.NEVER
+ if new_value:
+ cadence_value = self.get_default_cadence_value(app_name, noti_type)
+ assert type_prefs['email_cadence'] == cadence_value
+ else:
+ default_app_json = self.default_json[app_name]
+ default_notification_type_json = default_app_json['notification_types'][noti_type]
+ assert type_prefs[channel] == default_notification_type_json[channel]
+
+ def test_preference_not_updated_if_invalid_username(self):
+ """
+ Tests if no preference is updated when username is not valid
+ """
+ username = f"{self.user.username}-updated"
+ enc_username = encrypt_string(username)
+ enc_patch = encrypt_object({"value": True})
+ with self.assertNumQueries(1):
+ update_user_preferences_from_patch(enc_username, enc_patch)
diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py
index 07b1bf1330a5..4531cabda0ee 100644
--- a/openedx/core/djangoapps/notifications/email/utils.py
+++ b/openedx/core/djangoapps/notifications/email/utils.py
@@ -2,14 +2,22 @@
Email Notifications Utils
"""
import datetime
+import json
from django.conf import settings
+from django.urls import reverse
from pytz import utc
from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
from lms.djangoapps.branding.api import get_logo_url_for_email
+from lms.djangoapps.discussion.notification_prefs.views import UsernameCipher
+from openedx.core.djangoapps.notifications.base_notification import (
+ COURSE_NOTIFICATION_APPS,
+ COURSE_NOTIFICATION_TYPES,
+)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_EMAIL_NOTIFICATIONS
from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
+from openedx.core.djangoapps.notifications.models import CourseNotificationPreference
from xmodule.modulestore.django import modulestore
from .notification_icons import NotificationTypeIcons
@@ -51,7 +59,21 @@ def get_icon_url_for_notification_type(notification_type):
return NotificationTypeIcons.get_icon_url_for_notification_type(notification_type)
-def create_email_template_context():
+def get_unsubscribe_link(username, patch):
+ """
+ Returns unsubscribe url for username with patch preferences
+ """
+ encrypted_username = encrypt_string(username)
+ encrypted_patch = encrypt_object(patch)
+ kwargs = {
+ 'username': encrypted_username,
+ 'patch': encrypted_patch
+ }
+ relative_url = reverse('preference_update_from_encrypted_username_view', kwargs=kwargs)
+ return f"{settings.LMS_BASE}{relative_url}"
+
+
+def create_email_template_context(username):
"""
Creates email context for header and footer
"""
@@ -65,16 +87,21 @@ def create_email_template_context():
for social_platform in social_media_urls.keys()
if social_media_icons.get(social_platform)
}
+ patch = {
+ 'channel': 'email',
+ 'value': False
+ }
return {
"platform_name": settings.PLATFORM_NAME,
"mailing_address": settings.CONTACT_MAILING_ADDRESS,
"logo_url": get_logo_url_for_email(),
"social_media": social_media_info,
"notification_settings_url": f"{settings.ACCOUNT_MICROFRONTEND_URL}/notifications",
+ "unsubscribe_url": get_unsubscribe_link(username, patch)
}
-def create_email_digest_context(app_notifications_dict, start_date, end_date=None, digest_frequency="Daily",
+def create_email_digest_context(app_notifications_dict, username, start_date, end_date=None, digest_frequency="Daily",
courses_data=None):
"""
Creates email context based on content
@@ -84,7 +111,7 @@ def create_email_digest_context(app_notifications_dict, start_date, end_date=Non
digest_frequency: EmailCadence.DAILY or EmailCadence.WEEKLY
courses_data: Dictionary to cache course info (avoid additional db calls)
"""
- context = create_email_template_context()
+ context = create_email_template_context(username)
start_date_str = create_datetime_string(start_date)
end_date_str = create_datetime_string(end_date if end_date else start_date)
email_digest_updates = [{
@@ -243,3 +270,99 @@ def filter_notification_with_email_enabled_preferences(notifications, preference
if notification.notification_type in enabled_course_prefs[notification.course_id]:
filtered_notifications.append(notification)
return filtered_notifications
+
+
+def encrypt_string(string):
+ """
+ Encrypts input string
+ """
+ return UsernameCipher.encrypt(string)
+
+
+def decrypt_string(string):
+ """
+ Decrypts input string
+ """
+ return UsernameCipher.decrypt(string).decode()
+
+
+def encrypt_object(obj):
+ """
+ Returns hashed string of object
+ """
+ string = json.dumps(obj)
+ return encrypt_string(string)
+
+
+def decrypt_object(string):
+ """
+ Decrypts input string and returns an object
+ """
+ decoded = decrypt_string(string)
+ return json.loads(decoded)
+
+
+def update_user_preferences_from_patch(encrypted_username, encrypted_patch):
+ """
+ Decrypt username and patch and updates user preferences
+ Allowed parameters for decrypted patch
+ app_name: name of app
+ notification_type: notification type name
+ channel: channel name ('web', 'push', 'email')
+ value: True or False
+ course_id: course key string
+ """
+ username = decrypt_string(encrypted_username)
+ patch = decrypt_object(encrypted_patch)
+
+ app_value = patch.get("app_name")
+ type_value = patch.get("notification_type")
+ channel_value = patch.get("channel")
+ pref_value = bool(patch.get("value", False))
+
+ kwargs = {'user__username': username}
+ if 'course_id' in patch.keys():
+ kwargs['course_id'] = patch['course_id']
+
+ def is_name_match(name, param_name):
+ """
+ Name is match if strings are equal or param_name is None
+ """
+ return True if param_name is None else name == param_name
+
+ def is_editable(app_name, notification_type, channel):
+ """
+ Returns if notification type channel is editable
+ """
+ if notification_type == 'core':
+ return channel not in COURSE_NOTIFICATION_APPS[app_name]['non_editable']
+ return channel not in COURSE_NOTIFICATION_TYPES[notification_type]['non_editable']
+
+ def get_default_cadence_value(app_name, notification_type):
+ """
+ Returns default email cadence value
+ """
+ if notification_type == 'core':
+ return COURSE_NOTIFICATION_APPS[app_name]['core_email_cadence']
+ return COURSE_NOTIFICATION_TYPES[notification_type]['email_cadence']
+
+ preferences = CourseNotificationPreference.objects.filter(**kwargs)
+ # pylint: disable=too-many-nested-blocks
+ for preference in preferences:
+ preference_json = preference.notification_preference_config
+ for app_name, app_prefs in preference_json.items():
+ if not is_name_match(app_name, app_value):
+ continue
+ for noti_type, type_prefs in app_prefs['notification_types'].items():
+ if not is_name_match(noti_type, type_value):
+ continue
+ for channel in ['web', 'email', 'push']:
+ if not is_name_match(channel, channel_value):
+ continue
+ if is_editable(app_name, noti_type, channel):
+ type_prefs[channel] = pref_value
+ if channel == 'email':
+ cadence_value = get_default_cadence_value(app_name, noti_type)\
+ if pref_value else EmailCadence.NEVER
+ type_prefs['email_cadence'] = cadence_value
+ preference.save()
diff --git a/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html b/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html
index bcd5c0849346..0419b256656a 100644
--- a/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html
+++ b/openedx/core/djangoapps/notifications/templates/notifications/digest_footer.html
@@ -37,6 +37,9 @@
Notification Settings
+
+ Unsubscribe
+
© {% now "Y" %} {{ platform_name }}. All Rights Reserved
diff --git a/openedx/core/djangoapps/notifications/tests/test_views.py b/openedx/core/djangoapps/notifications/tests/test_views.py
index b90d22063874..413c1ce1521b 100644
--- a/openedx/core/djangoapps/notifications/tests/test_views.py
+++ b/openedx/core/djangoapps/notifications/tests/test_views.py
@@ -7,6 +7,7 @@
import ddt
from django.conf import settings
+from django.test.utils import override_settings
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from openedx_events.learning.data import CourseData, CourseEnrollmentData, UserData, UserPersonalData
@@ -26,8 +27,10 @@
FORUM_ROLE_MODERATOR
)
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFICATIONS
+from openedx.core.djangoapps.notifications.email_notifications import EmailCadence
from openedx.core.djangoapps.notifications.models import CourseNotificationPreference, Notification
from openedx.core.djangoapps.notifications.serializers import NotificationCourseEnrollmentSerializer
+from openedx.core.djangoapps.notifications.email.utils import get_unsubscribe_link
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
@@ -1080,6 +1083,40 @@ def test_mark_notification_read_without_app_name_and_notification_id(self):
self.assertEqual(response.data, {'error': 'Invalid app_name or notification_id.'})
+@ddt.ddt
+class UpdatePreferenceFromEncryptedDataView(ModuleStoreTestCase):
+ """
+ Tests if preference is updated when encrypted url is hit
+ """
+ def setUp(self):
+ """
+ Setup test case
+ """
+ super().setUp()
+ password = 'password'
+ self.user = UserFactory(password=password)
+ self.client.login(username=self.user.username, password=password)
+ self.course = CourseFactory.create(display_name='test course 1', run="Testing_course_1")
+ CourseNotificationPreference(course_id=self.course.id, user=self.user).save()
+
+ @override_settings(LMS_BASE="")
+ @ddt.data('get', 'post')
+ def test_if_preference_is_updated(self, request_type):
+ """
+ Tests if preference is updated when url is hit
+ """
+ url = get_unsubscribe_link(self.user.username, {'channel': 'email', 'value': False})
+ func = getattr(self.client, request_type)
+ response = func(url)
+ assert response.status_code == status.HTTP_200_OK
+ preference = CourseNotificationPreference.objects.get(user=self.user, course_id=self.course.id)
+ config = preference.notification_preference_config
+ for app_name, app_prefs in config.items():
+ for type_prefs in app_prefs['notification_types'].values():
+ assert type_prefs['email'] is False
+ assert type_prefs['email_cadence'] == EmailCadence.NEVER
+
+
def remove_notifications_with_visibility_settings(expected_response):
"""
Remove notifications with visibility settings from the expected response.
diff --git a/openedx/core/djangoapps/notifications/urls.py b/openedx/core/djangoapps/notifications/urls.py
index 89b04443a581..9904010fb33f 100644
--- a/openedx/core/djangoapps/notifications/urls.py
+++ b/openedx/core/djangoapps/notifications/urls.py
@@ -11,7 +11,9 @@
NotificationCountView,
NotificationListAPIView,
NotificationReadAPIView,
- UserNotificationPreferenceView, UserNotificationChannelPreferenceView,
+ UserNotificationChannelPreferenceView,
+ UserNotificationPreferenceView,
+ preference_update_from_encrypted_username_view,
)
router = routers.DefaultRouter()
@@ -37,7 +39,8 @@
name='mark-notifications-seen'
),
path('read/', NotificationReadAPIView.as_view(), name='notifications-read'),
-
+ path('preferences/update///', preference_update_from_encrypted_username_view,
+ name='preference_update_from_encrypted_username_view'),
]
urlpatterns += router.urls
diff --git a/openedx/core/djangoapps/notifications/views.py b/openedx/core/djangoapps/notifications/views.py
index 547847f55e55..0c3f4d0ba945 100644
--- a/openedx/core/djangoapps/notifications/views.py
+++ b/openedx/core/djangoapps/notifications/views.py
@@ -5,16 +5,19 @@
from django.conf import settings
from django.db.models import Count
+from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext as _
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
from rest_framework import generics, status
+from rest_framework.decorators import api_view
from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response
from rest_framework.views import APIView
from common.djangoapps.student.models import CourseEnrollment
+from openedx.core.djangoapps.notifications.email.utils import update_user_preferences_from_patch
from openedx.core.djangoapps.notifications.models import (
CourseNotificationPreference,
get_course_notification_preference_config_version
@@ -479,3 +482,13 @@ def patch(self, request, *args, **kwargs):
return Response({'message': _('Notifications marked read.')}, status=status.HTTP_200_OK)
return Response({'error': _('Invalid app_name or notification_id.')}, status=status.HTTP_400_BAD_REQUEST)
+
+
+@api_view(['GET', 'POST'])
+def preference_update_from_encrypted_username_view(request, username, patch):
+ """
+ View to update user preferences from encrypted username and patch.
+ username and patch must be string
+ """
+ update_user_preferences_from_patch(username, patch)
+ return HttpResponse("Success", status=status.HTTP_200_OK)