From 7c42a600daa9cd207729ff171a489c57cf9b6efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Berm=C3=BAdez-Mendoza?= Date: Wed, 5 Nov 2025 21:49:23 +0100 Subject: [PATCH 1/4] feat: make LEARNING_MICROFRONTEND_URL site aware. --- common/djangoapps/student/tasks.py | 3 +- common/djangoapps/student/tests/test_tasks.py | 41 +++++++++++++++++ .../commands/goal_reminder_email.py | 6 ++- .../tests/test_goal_reminder_email.py | 36 +++++++++++++++ .../enrollments/enrollments_notifications.py | 7 ++- .../tests/test_enrollments_notifications.py | 46 +++++++++++++++++-- .../notifications/email/tests/test_utils.py | 32 +++++++++++++ .../djangoapps/notifications/email/utils.py | 8 +++- .../tests/test_url_helpers.py | 14 ++++-- .../features/course_experience/url_helpers.py | 26 +++++++++-- 10 files changed, 202 insertions(+), 17 deletions(-) diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py index c13c5bf96b..2d436c7c9b 100644 --- a/common/djangoapps/student/tasks.py +++ b/common/djangoapps/student/tasks.py @@ -68,7 +68,8 @@ def send_course_enrollment_email( "LMS_ROOT_URL", settings.LMS_ROOT_URL ), "learning_base_url": configuration_helpers.get_value( - "LEARNING_MICROFRONTEND_URL", settings.LEARNING_MICROFRONTEND_URL + "LEARNING_MICROFRONTEND_URL", + settings.LEARNING_MICROFRONTEND_URL, ), "track_mode": track_mode } diff --git a/common/djangoapps/student/tests/test_tasks.py b/common/djangoapps/student/tests/test_tasks.py index d5fc3d3db0..0921e4684b 100644 --- a/common/djangoapps/student/tests/test_tasks.py +++ b/common/djangoapps/student/tests/test_tasks.py @@ -388,3 +388,44 @@ class TestCourseEnrollmentEmailTask(ModuleStoreTestCase): ) pytest.raises(Exception, task.get) self.assertEqual(mock_get_email_client.call_count, (MAX_RETRIES + 1)) + + @patch("common.djangoapps.student.tasks.get_course_uuid_for_course") + @patch("common.djangoapps.student.tasks.get_owners_for_course") + @patch("common.djangoapps.student.tasks.get_course_run_details") + @patch("common.djangoapps.student.tasks.get_course_dates_for_email") + @patch("common.djangoapps.student.tasks.get_email_client") + def test_learning_base_url_uses_site_config( + self, + mock_get_email_client, + mock_get_course_dates_for_email, + mock_get_course_run_details, + mock_get_owners_for_course, + mock_get_course_uuid_for_course, + ): + """Test Braze payload sets learning_base_url from site-configured MFE URL.""" + mock_get_course_uuid_for_course.return_value = self.course_uuid + mock_get_owners_for_course.return_value = self._get_course_owners() + mock_get_course_run_details.return_value = self._get_course_run() + mock_get_course_dates_for_email.return_value = self._get_course_dates() + + siteconf_url = "https://learningmfe.siteconf" + + def _get_value(key, default): + return siteconf_url if key == "LEARNING_MICROFRONTEND_URL" else default + + with patch( + "common.djangoapps.student.tasks.configuration_helpers.get_value", + side_effect=_get_value, + ) as mock_get_value: + send_course_enrollment_email.apply_async(kwargs=self.send_course_enrollment_email_kwargs) + + expected = self._get_canvas_properties() + expected["learning_base_url"] = siteconf_url + + mock_get_email_client.return_value.send_canvas_message.assert_called_with( + canvas_id=BRAZE_COURSE_ENROLLMENT_CANVAS_ID, + recipients=[{"external_user_id": self.user.id}], + canvas_entry_properties=expected, + ) + mock_get_value.assert_any_call("LEARNING_MICROFRONTEND_URL", settings.LEARNING_MICROFRONTEND_URL) + mock_get_value.assert_any_call("LMS_ROOT_URL", settings.LMS_ROOT_URL) diff --git a/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py b/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py index e43fedcb6b..4b6f37703e 100644 --- a/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py +++ b/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py @@ -74,8 +74,10 @@ def send_ace_message(goal, session_id): course_home_url = get_learning_mfe_home_url(course_key=goal.course_key, url_fragment='home') - goals_unsubscribe_url = f'{settings.LEARNING_MICROFRONTEND_URL}/goal-unsubscribe/{goal.unsubscribe_token}' - + goals_unsubscribe_url = ( + f'{configuration_helpers.get_value("LEARNING_MICROFRONTEND_URL", settings.LEARNING_MICROFRONTEND_URL)}' + f'/goal-unsubscribe/{goal.unsubscribe_token}' + ) language = get_user_preference(user, LANGUAGE_KEY) # Code to allow displaying different banner images for different languages diff --git a/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py b/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py index 46d46832a6..915c0f584c 100644 --- a/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py +++ b/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py @@ -235,6 +235,42 @@ class TestGoalReminderEmailCommand(TestCase): send_ace_message(goal, str(uuid.uuid4())) assert mock_ace.called is value + def test_goals_unsubscribe_url_uses_site_config(self): + """Test goals unsubscribe URL uses site-configured MFE base.""" + goal = self.make_valid_goal() + with mock.patch('lms.djangoapps.course_goals.management.commands.goal_reminder_email.ace.send') as mock_ace, \ + mock.patch( + 'lms.djangoapps.course_goals.management.commands.goal_reminder_email.configuration_helpers.get_value', + return_value='https://learning.siteconf', + ) as mock_get_value: + assert send_ace_message(goal, str(uuid.uuid4())) is True + + assert mock_ace.call_count == 1 + msg = mock_ace.call_args[0][0] + assert msg.context[ + 'goals_unsubscribe_url' + ] == f'https://learning.siteconf/goal-unsubscribe/{goal.unsubscribe_token}' + mock_get_value.assert_any_call('LEARNING_MICROFRONTEND_URL', settings.LEARNING_MICROFRONTEND_URL) + + def test_goals_unsubscribe_url_falls_back_to_settings(self): + """Test goals unsubscribe URL falls back to settings when site config is absent.""" + default_url = 'https://learning.default' + goal = self.make_valid_goal() + with override_settings(LEARNING_MICROFRONTEND_URL=default_url): + with mock.patch( + 'lms.djangoapps.course_goals.management.commands.goal_reminder_email.ace.send', + ) as mock_ace, \ + mock.patch( + 'lms.djangoapps.course_goals.management.commands.goal_reminder_email.configuration_helpers.get_value', + side_effect=lambda k, d: d, + ) as mock_get_value: + assert send_ace_message(goal, str(uuid.uuid4())) is True + + assert mock_ace.call_count == 1 + msg = mock_ace.call_args[0][0] + assert msg.context['goals_unsubscribe_url'] == f'{default_url}/goal-unsubscribe/{goal.unsubscribe_token}' + mock_get_value.assert_any_call('LEARNING_MICROFRONTEND_URL', default_url) + class TestGoalReminderEmailSES(TestCase): """ diff --git a/openedx/core/djangoapps/enrollments/enrollments_notifications.py b/openedx/core/djangoapps/enrollments/enrollments_notifications.py index 90f8a42d97..e1f92efeec 100644 --- a/openedx/core/djangoapps/enrollments/enrollments_notifications.py +++ b/openedx/core/djangoapps/enrollments/enrollments_notifications.py @@ -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, ) diff --git a/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py b/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py index 5420733107..aebbd96708 100644 --- a/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py +++ b/openedx/core/djangoapps/enrollments/tests/test_enrollments_notifications.py @@ -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) diff --git a/openedx/core/djangoapps/notifications/email/tests/test_utils.py b/openedx/core/djangoapps/notifications/email/tests/test_utils.py index 75c70ae194..3f4d526dc7 100644 --- a/openedx/core/djangoapps/notifications/email/tests/test_utils.py +++ b/openedx/core/djangoapps/notifications/email/tests/test_utils.py @@ -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): diff --git a/openedx/core/djangoapps/notifications/email/utils.py b/openedx/core/djangoapps/notifications/email/utils.py index 1f761c0860..5298132b51 100644 --- a/openedx/core/djangoapps/notifications/email/utils.py +++ b/openedx/core/djangoapps/notifications/email/utils.py @@ -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): diff --git a/openedx/features/course_experience/tests/test_url_helpers.py b/openedx/features/course_experience/tests/test_url_helpers.py index b6f134f780..96baaeb2a5 100644 --- a/openedx/features/course_experience/tests/test_url_helpers.py +++ b/openedx/features/course_experience/tests/test_url_helpers.py @@ -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'] diff --git a/openedx/features/course_experience/url_helpers.py b/openedx/features/course_experience/url_helpers.py index 44cfdb4aa1..9c5484de10 100644 --- a/openedx/features/course_experience/url_helpers.py +++ b/openedx/features/course_experience/url_helpers.py @@ -17,9 +17,24 @@ 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() +def _learning_mfe_base_url() -> str: + """ + Return the site-aware base URL for the Learning MFE. + + Reads `LEARNING_MICROFRONTEND_URL` from Site Configuration when available; + otherwise falls back to `settings.LEARNING_MICROFRONTEND_URL`. + """ + return configuration_helpers.get_value( + 'LEARNING_MICROFRONTEND_URL', + settings.LEARNING_MICROFRONTEND_URL, + ) + + def get_courseware_url( usage_key: UsageKey, request: Optional[HttpRequest] = None, @@ -124,7 +139,7 @@ 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}' + mfe_link = f'{_learning_mfe_base_url()}/course/{course_key}' get_params = params.copy() if params else None if preview: @@ -134,7 +149,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_mfe_base_url()}/preview/course/{course_key}' if sequence_key: mfe_link += f'/{sequence_key}' @@ -164,7 +179,7 @@ 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}' + mfe_link = f'{_learning_mfe_base_url()}/course/{course_key}' if url_fragment: mfe_link += f'/{url_fragment}' @@ -179,9 +194,10 @@ 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 = _learning_mfe_base_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) From 450f881308f0b0495ea02ea5504fa069ad56edf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Berm=C3=BAdez-Mendoza?= Date: Thu, 6 Nov 2025 16:59:25 +0100 Subject: [PATCH 2/4] chore: address comment by Copilot. --- .../management/commands/goal_reminder_email.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py b/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py index 4b6f37703e..287c613d89 100644 --- a/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py +++ b/lms/djangoapps/course_goals/management/commands/goal_reminder_email.py @@ -73,11 +73,11 @@ def send_ace_message(goal, session_id): message_context = get_base_template_context(site) course_home_url = get_learning_mfe_home_url(course_key=goal.course_key, url_fragment='home') - - goals_unsubscribe_url = ( - f'{configuration_helpers.get_value("LEARNING_MICROFRONTEND_URL", settings.LEARNING_MICROFRONTEND_URL)}' - f'/goal-unsubscribe/{goal.unsubscribe_token}' + learning_microfrontend_url = configuration_helpers.get_value( + 'LEARNING_MICROFRONTEND_URL', + settings.LEARNING_MICROFRONTEND_URL, ) + goals_unsubscribe_url = f'{learning_microfrontend_url}/goal-unsubscribe/{goal.unsubscribe_token}' language = get_user_preference(user, LANGUAGE_KEY) # Code to allow displaying different banner images for different languages From 6b821a6a5f1ebcc32cb31ec0b1d616ef25ea2bcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Berm=C3=BAdez-Mendoza?= Date: Thu, 6 Nov 2025 18:22:33 +0100 Subject: [PATCH 3/4] chore: address comments by Feanil and fix pycodestyle. --- .../tests/test_goal_reminder_email.py | 21 ++++++------ .../features/course_experience/url_helpers.py | 32 +++++++++---------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py b/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py index 915c0f584c..e243263124 100644 --- a/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py +++ b/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py @@ -238,12 +238,12 @@ class TestGoalReminderEmailCommand(TestCase): def test_goals_unsubscribe_url_uses_site_config(self): """Test goals unsubscribe URL uses site-configured MFE base.""" goal = self.make_valid_goal() - with mock.patch('lms.djangoapps.course_goals.management.commands.goal_reminder_email.ace.send') as mock_ace, \ - mock.patch( + with mock.patch('lms.djangoapps.course_goals.management.commands.goal_reminder_email.ace.send') as mock_ace: + with mock.patch( 'lms.djangoapps.course_goals.management.commands.goal_reminder_email.configuration_helpers.get_value', return_value='https://learning.siteconf', ) as mock_get_value: - assert send_ace_message(goal, str(uuid.uuid4())) is True + assert send_ace_message(goal, str(uuid.uuid4())) is True assert mock_ace.call_count == 1 msg = mock_ace.call_args[0][0] @@ -259,12 +259,15 @@ class TestGoalReminderEmailCommand(TestCase): with override_settings(LEARNING_MICROFRONTEND_URL=default_url): with mock.patch( 'lms.djangoapps.course_goals.management.commands.goal_reminder_email.ace.send', - ) as mock_ace, \ - mock.patch( - 'lms.djangoapps.course_goals.management.commands.goal_reminder_email.configuration_helpers.get_value', - side_effect=lambda k, d: d, - ) as mock_get_value: - assert send_ace_message(goal, str(uuid.uuid4())) is True + ) as mock_ace: + with mock.patch( + ( + 'lms.djangoapps.course_goals.management.commands.' + 'goal_reminder_email.configuration_helpers.get_value' + ), + side_effect=lambda k, d: d, + ) as mock_get_value: + assert send_ace_message(goal, str(uuid.uuid4())) is True assert mock_ace.call_count == 1 msg = mock_ace.call_args[0][0] diff --git a/openedx/features/course_experience/url_helpers.py b/openedx/features/course_experience/url_helpers.py index 9c5484de10..e70019dbe2 100644 --- a/openedx/features/course_experience/url_helpers.py +++ b/openedx/features/course_experience/url_helpers.py @@ -22,19 +22,6 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_ User = get_user_model() -def _learning_mfe_base_url() -> str: - """ - Return the site-aware base URL for the Learning MFE. - - Reads `LEARNING_MICROFRONTEND_URL` from Site Configuration when available; - otherwise falls back to `settings.LEARNING_MICROFRONTEND_URL`. - """ - return configuration_helpers.get_value( - 'LEARNING_MICROFRONTEND_URL', - settings.LEARNING_MICROFRONTEND_URL, - ) - - def get_courseware_url( usage_key: UsageKey, request: Optional[HttpRequest] = None, @@ -139,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'{_learning_mfe_base_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: @@ -149,7 +140,7 @@ def make_learning_mfe_courseware_url( get_params = None if (unit_key or sequence_key): - mfe_link = f'{_learning_mfe_base_url()}/preview/course/{course_key}' + mfe_link = f'{learning_microfrontend_url}/preview/course/{course_key}' if sequence_key: mfe_link += f'/{sequence_key}' @@ -179,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'{_learning_mfe_base_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}' @@ -194,7 +189,10 @@ def is_request_from_learning_mfe(request: HttpRequest): """ Returns whether the given request was made by the frontend-app-learning MFE. """ - url_str = _learning_mfe_base_url() + url_str = configuration_helpers.get_value( + 'LEARNING_MICROFRONTEND_URL', + settings.LEARNING_MICROFRONTEND_URL, + ) if not url_str: return False From ce0ef12c01f8abe6888deeed4af8cab1cc331cc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Berm=C3=BAdez-Mendoza?= Date: Thu, 6 Nov 2025 19:32:26 +0100 Subject: [PATCH 4/4] chore: fix missing override_settings import. --- .../management/commands/tests/test_goal_reminder_email.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py b/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py index e243263124..d78031fa48 100644 --- a/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py +++ b/lms/djangoapps/course_goals/management/commands/tests/test_goal_reminder_email.py @@ -12,6 +12,7 @@ import ddt from django.conf import settings from django.core.management import call_command from django.test import TestCase +from django.test.utils import override_settings from edx_toggles.toggles.testutils import override_waffle_flag from freezegun import freeze_time from waffle import get_waffle_flag_model # pylint: disable=invalid-django-waffle-import