Merge pull request #37504 from Pearson-Advance/felipeb/learning-microfrontend-url-site-aware

feat: make LEARNING_MICROFRONTEND_URL site aware.
This commit is contained in:
Feanil Patel
2025-12-23 10:58:34 -05:00
committed by GitHub
10 changed files with 205 additions and 18 deletions

View File

@@ -5,6 +5,7 @@ from django.conf import settings
from openedx_events.learning.data import UserNotificationData
from openedx_events.learning.signals import USER_NOTIFICATION_REQUESTED
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
class EnrollmentNotificationSender:
@@ -21,6 +22,10 @@ class EnrollmentNotificationSender:
"""
Send audit access expiring soon notification to user
"""
learning_microfrontend_url = configuration_helpers.get_value(
'LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL,
)
notification_data = UserNotificationData(
user_ids=[int(self.user_id)],
@@ -29,7 +34,7 @@ class EnrollmentNotificationSender:
'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",
content_url=f"{learning_microfrontend_url}/course/{str(self.course.id)}/home",
app_name="enrollments",
course_key=self.course.id,
)

View File

@@ -5,12 +5,15 @@ import unittest
import datetime
from unittest.mock import MagicMock, patch
from django.conf import settings
from django.test.utils import override_settings
import pytest
from openedx.core.djangoapps.enrollments.enrollments_notifications import EnrollmentNotificationSender
from openedx_events.learning.data import UserNotificationData
DEFAULT_MFE_URL = "https://learning.default"
SITE_CONF_MFE_URL = "https://learning.siteconf"
@pytest.mark.django_db
class TestEnrollmentsNotificationSender(unittest.TestCase):
@@ -26,14 +29,20 @@ class TestEnrollmentsNotificationSender(unittest.TestCase):
self.user_id = '123'
self.notification_sender = EnrollmentNotificationSender(self.course, self.user_id, self.expiry_date)
@override_settings(LEARNING_MICROFRONTEND_URL=DEFAULT_MFE_URL)
@patch(
'openedx.core.djangoapps.enrollments.enrollments_notifications.configuration_helpers.get_value',
return_value=SITE_CONF_MFE_URL,
)
@patch('openedx.core.djangoapps.enrollments.enrollments_notifications.USER_NOTIFICATION_REQUESTED.send_event')
def test_send_audit_access_expiring_soon_notification(self, mock_send_notification):
def test_send_audit_access_expiring_soon_notification(self, mock_send_notification, mock_get_value):
"""
Test that audit access expiring soon notification event is sent with correct parameters.
"""
self.notification_sender.send_audit_access_expiring_soon_notification()
mock_get_value.assert_called_once_with('LEARNING_MICROFRONTEND_URL', DEFAULT_MFE_URL)
mock_send_notification.assert_called_once()
notification_data = UserNotificationData(
user_ids=[int(self.user_id)],
@@ -42,8 +51,39 @@ class TestEnrollmentsNotificationSender(unittest.TestCase):
'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",
content_url=f"{SITE_CONF_MFE_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)
@override_settings(LEARNING_MICROFRONTEND_URL=DEFAULT_MFE_URL)
@patch(
'openedx.core.djangoapps.enrollments.enrollments_notifications.configuration_helpers.get_value',
side_effect=lambda key, default: default,
)
@patch('openedx.core.djangoapps.enrollments.enrollments_notifications.USER_NOTIFICATION_REQUESTED.send_event')
def test_send_audit_access_expiring_soon_notification_falls_back_to_settings(
self,
mock_send_event,
mock_get_value
):
"""
Test mocks missing site-config value and verifies default URL and get_value args.
"""
self.notification_sender.send_audit_access_expiring_soon_notification()
mock_get_value.assert_called_once_with('LEARNING_MICROFRONTEND_URL', DEFAULT_MFE_URL)
expected_notification = 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"{DEFAULT_MFE_URL}/course/{str(self.course.id)}/home",
app_name="enrollments",
course_key=self.course.id,
)
mock_send_event.assert_called_once_with(notification_data=expected_notification)

View File

@@ -6,6 +6,9 @@ import ddt
import pytest
from django.http.response import Http404
from django.conf import settings
from django.test.utils import override_settings
from unittest.mock import patch
from pytz import utc
from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import
@@ -27,6 +30,7 @@ from openedx.core.djangoapps.notifications.email.utils import (
encrypt_string,
get_course_info,
get_time_ago,
get_unsubscribe_link,
is_email_notification_flag_enabled,
update_user_preferences_from_patch,
)
@@ -93,9 +97,37 @@ class TestUtilFunctions(ModuleStoreTestCase):
assert "1w" == get_time_ago(current_datetime - datetime.timedelta(days=7))
def test_datetime_string(self):
"""Test datetime is formatted as 'Weekday, Mon DD'."""
dt = datetime.datetime(2024, 3, 25)
assert create_datetime_string(dt) == "Monday, Mar 25"
def test_get_unsubscribe_link_uses_site_config(self):
"""Test unsubscribe link uses site-configured MFE URL and encrypted username."""
with patch('openedx.core.djangoapps.notifications.email.utils.configuration_helpers.get_value',
return_value='https://learning.siteconf') as mock_get_value, \
patch('openedx.core.djangoapps.notifications.email.utils.encrypt_string',
return_value='ENC') as mock_encrypt:
url = get_unsubscribe_link(self.user.username)
assert url == 'https://learning.siteconf/preferences-unsubscribe/ENC/'
mock_get_value.assert_called_once_with('LEARNING_MICROFRONTEND_URL', settings.LEARNING_MICROFRONTEND_URL)
mock_encrypt.assert_called_once_with(self.user.username)
def test_get_unsubscribe_link_falls_back_to_settings(self):
"""Test unsubscribe link falls back to settings when site config is absent."""
default_url = 'https://learning.default'
with override_settings(LEARNING_MICROFRONTEND_URL=default_url):
with patch('openedx.core.djangoapps.notifications.email.utils.configuration_helpers.get_value',
side_effect=lambda k, d: d) as mock_get_value, \
patch('openedx.core.djangoapps.notifications.email.utils.encrypt_string',
return_value='ENC') as mock_encrypt:
url = get_unsubscribe_link(self.user.username)
assert url == f'{default_url}/preferences-unsubscribe/ENC/'
mock_get_value.assert_called_once_with('LEARNING_MICROFRONTEND_URL', default_url)
mock_encrypt.assert_called_once_with(self.user.username)
@ddt.ddt
class TestContextFunctions(ModuleStoreTestCase):

View File

@@ -22,6 +22,7 @@ from openedx.core.djangoapps.notifications.email_notifications import EmailCaden
from openedx.core.djangoapps.notifications.events import notification_preference_unsubscribe_event
from openedx.core.djangoapps.notifications.models import NotificationPreference
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.modulestore.django import modulestore
from .notification_icons import NotificationTypeIcons
@@ -71,7 +72,12 @@ def get_unsubscribe_link(username):
Returns unsubscribe url for username with patch preferences
"""
encrypted_username = encrypt_string(username)
return f"{settings.LEARNING_MICROFRONTEND_URL}/preferences-unsubscribe/{encrypted_username}/"
learning_microfrontend_url = configuration_helpers.get_value(
'LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL,
)
return f'{learning_microfrontend_url}/preferences-unsubscribe/{encrypted_username}/'
def create_email_template_context(username):

View File

@@ -5,6 +5,7 @@ import ddt
from django.test import TestCase
from django.test.client import RequestFactory
from django.test.utils import override_settings
from unittest.mock import patch
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory
@@ -33,10 +34,14 @@ class IsLearningMfeTests(TestCase):
)
@ddt.unpack
def test_is_request_from_learning_mfe(self, mfe_url, referrer_url, is_mfe):
"""
Test requests are marked as from the Learning MFE when HTTP_REFERER matches LEARNING_MICROFRONTEND_URL.
"""
with override_settings(LEARNING_MICROFRONTEND_URL=mfe_url):
request = self.request_factory.get('/course')
request.META['HTTP_REFERER'] = referrer_url
assert url_helpers.is_request_from_learning_mfe(request) == is_mfe
with patch.object(url_helpers.configuration_helpers, 'get_value', return_value=mfe_url):
request = self.request_factory.get('/course')
request.META['HTTP_REFERER'] = referrer_url
assert url_helpers.is_request_from_learning_mfe(request) == is_mfe
@ddt.ddt
@@ -158,7 +163,8 @@ class GetCoursewareUrlTests(SharedModuleStoreTestCase):
check that the expected path (URL without querystring) is returned by `get_courseware_url`.
"""
block = self.items[structure_level]
url = url_helpers.get_courseware_url(block.location)
with patch.object(url_helpers.configuration_helpers, 'get_value', return_value='http://learning-mfe'):
url = url_helpers.get_courseware_url(block.location)
path = url.split('?')[0]
assert path == expected_path
course_run = self.items['course_run']

View File

@@ -17,6 +17,8 @@ from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.search import path_to_location # lint-amnesty, pylint: disable=wrong-import-order
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
User = get_user_model()
@@ -124,7 +126,11 @@ def make_learning_mfe_courseware_url(
strings. They're only ever used to concatenate a URL string.
`params` is an optional QueryDict object (e.g. request.GET)
"""
mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/course/{course_key}'
learning_microfrontend_url = configuration_helpers.get_value(
'LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL,
)
mfe_link = f'{learning_microfrontend_url}/course/{course_key}'
get_params = params.copy() if params else None
if preview:
@@ -134,7 +140,7 @@ def make_learning_mfe_courseware_url(
get_params = None
if (unit_key or sequence_key):
mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/preview/course/{course_key}'
mfe_link = f'{learning_microfrontend_url}/preview/course/{course_key}'
if sequence_key:
mfe_link += f'/{sequence_key}'
@@ -164,7 +170,11 @@ def get_learning_mfe_home_url(
`url_fragment` is an optional string.
`params` is an optional QueryDict object (e.g. request.GET)
"""
mfe_link = f'{settings.LEARNING_MICROFRONTEND_URL}/course/{course_key}'
learning_microfrontend_url = configuration_helpers.get_value(
'LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL,
)
mfe_link = f'{learning_microfrontend_url}/course/{course_key}'
if url_fragment:
mfe_link += f'/{url_fragment}'
@@ -179,9 +189,13 @@ def is_request_from_learning_mfe(request: HttpRequest):
"""
Returns whether the given request was made by the frontend-app-learning MFE.
"""
if not settings.LEARNING_MICROFRONTEND_URL:
url_str = configuration_helpers.get_value(
'LEARNING_MICROFRONTEND_URL',
settings.LEARNING_MICROFRONTEND_URL,
)
if not url_str:
return False
url = urlparse(settings.LEARNING_MICROFRONTEND_URL)
url = urlparse(url_str)
mfe_url_base = f'{url.scheme}://{url.netloc}'
return request.META.get('HTTP_REFERER', '').startswith(mfe_url_base)