Skip to content

Commit 7dd54dc

Browse files
feat: add instructor dashboard SUPPORT_URL legacy fallback to MFE_CONFIG (#38446)
Adds a legacy config override layer so the instructor dashboard gets its SUPPORT_URL from the help-tokens package when no explicit MFE_CONFIG_OVERRIDES entry is set. This supports the frontend-base help button (openedx/frontend-base#245). Also adds instructor-dashboard to MFE_NAME_TO_APP_ID and refactors get_mfe_config_overrides into separate legacy/explicit/merged helpers. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ec13a6d commit 7dd54dc

2 files changed

Lines changed: 154 additions & 10 deletions

File tree

lms/djangoapps/mfe_config_api/tests/test_views.py

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,45 @@ def side_effect(key, default=None):
297297
# Value in original MFE_CONFIG not overridden by catalog config should be preserved
298298
self.assertEqual(data["PRESERVED_SETTING"], "preserved") # noqa: PT009
299299

300+
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
301+
def test_legacy_overrides_instructor_dashboard(self, configuration_helpers_mock):
302+
"""Legacy help-tokens SUPPORT_URL is included for instructor-dashboard when no explicit override is set."""
303+
def side_effect(key, default=None):
304+
if key == "MFE_CONFIG":
305+
return {"LMS_BASE_URL": "https://courses.example.com"}
306+
if key == "MFE_CONFIG_OVERRIDES":
307+
return {}
308+
return default
309+
configuration_helpers_mock.get_value.side_effect = side_effect
310+
311+
response = self.client.get(f"{self.mfe_config_api_url}?mfe=instructor-dashboard")
312+
self.assertEqual(response.status_code, status.HTTP_200_OK) # noqa: PT009
313+
data = response.json()
314+
self.assertEqual( # noqa: PT009
315+
data["SUPPORT_URL"],
316+
"https://docs.openedx.org/en/latest/educators/index.html",
317+
)
318+
319+
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
320+
def test_explicit_override_wins_over_legacy_overrides(self, configuration_helpers_mock):
321+
"""An explicit SUPPORT_URL in MFE_CONFIG_OVERRIDES wins over the help-tokens fallback."""
322+
def side_effect(key, default=None):
323+
if key == "MFE_CONFIG":
324+
return {"LMS_BASE_URL": "https://courses.example.com"}
325+
if key == "MFE_CONFIG_OVERRIDES":
326+
return {
327+
"instructor-dashboard": {
328+
"SUPPORT_URL": "https://help.example.com/instructor",
329+
},
330+
}
331+
return default
332+
configuration_helpers_mock.get_value.side_effect = side_effect
333+
334+
response = self.client.get(f"{self.mfe_config_api_url}?mfe=instructor-dashboard")
335+
self.assertEqual(response.status_code, status.HTTP_200_OK) # noqa: PT009
336+
data = response.json()
337+
self.assertEqual(data["SUPPORT_URL"], "https://help.example.com/instructor") # noqa: PT009
338+
300339

301340
class MfeNameToAppIdTests(SimpleTestCase):
302341
"""Tests for the mfe_name_to_app_id helper."""
@@ -317,6 +356,12 @@ def test_mapped_alias(self):
317356
"org.openedx.frontend.app.authoring",
318357
)
319358

359+
def test_instructor_dashboard(self):
360+
self.assertEqual( # noqa: PT009
361+
mfe_name_to_app_id("instructor-dashboard"),
362+
"org.openedx.frontend.app.instructorDashboard",
363+
)
364+
320365
def test_fallback_for_unknown_name(self):
321366
"""Unknown names fall back to programmatic kebab-to-camelCase conversion."""
322367
self.assertEqual( # noqa: PT009
@@ -420,8 +465,9 @@ def side_effect(key, default=None):
420465
for legacy_key in default_legacy_config:
421466
self.assertIn(legacy_key, common) # noqa: PT009
422467

468+
@patch("lms.djangoapps.mfe_config_api.views.get_legacy_config_overrides", return_value={})
423469
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
424-
def test_apps_from_overrides(self, configuration_helpers_mock):
470+
def test_apps_from_overrides(self, configuration_helpers_mock, _legacy_overrides_mock): # noqa: PT019
425471
"""Each MFE_CONFIG_OVERRIDES entry becomes an app with shared base config + overrides."""
426472
mfe_config_overrides = {
427473
"authn": {
@@ -521,9 +567,10 @@ def side_effect(key, default=None):
521567
self.assertNotIn("BASE_URL", data["commonAppConfig"]) # noqa: PT009
522568
self.assertNotIn("LOGIN_URL", data["commonAppConfig"]) # noqa: PT009
523569

570+
@patch("lms.djangoapps.mfe_config_api.views.get_legacy_config_overrides", return_value={})
524571
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
525-
def test_no_apps_when_no_overrides(self, configuration_helpers_mock):
526-
"""The apps key is omitted when MFE_CONFIG_OVERRIDES is empty."""
572+
def test_no_apps_when_no_overrides(self, configuration_helpers_mock, _legacy_overrides_mock): # noqa: PT019
573+
"""The apps key is omitted when MFE_CONFIG_OVERRIDES is empty and no legacy overrides are present."""
527574
def side_effect(key, default=None):
528575
if key == "MFE_CONFIG":
529576
return {"LMS_BASE_URL": "https://courses.example.com"}
@@ -566,8 +613,9 @@ def side_effect(key, default=None):
566613
self.assertEqual(common["CREDENTIALS_BASE_URL"], "https://credentials.example.com") # noqa: PT009
567614
self.assertEqual(common["STUDIO_BASE_URL"], "https://studio.example.com") # noqa: PT009
568615

616+
@patch("lms.djangoapps.mfe_config_api.views.get_legacy_config_overrides", return_value={})
569617
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
570-
def test_invalid_override_entry_skipped(self, configuration_helpers_mock):
618+
def test_invalid_override_entry_skipped(self, configuration_helpers_mock, _legacy_overrides_mock): # noqa: PT019
571619
"""Non-dict override entries are silently skipped."""
572620
mfe_config_overrides = {
573621
"authn": {"SOME_KEY": "value"},
@@ -720,3 +768,49 @@ def side_effect(key, default=None):
720768
# Brand new app from FRONTEND_SITE_CONFIG is appended
721769
brand_new = apps_by_id["org.openedx.frontend.app.brand.new"]["config"]
722770
self.assertEqual(brand_new["BRAND_NEW_KEY"], "value") # noqa: PT009
771+
772+
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
773+
def test_legacy_overrides_instructor_dashboard_support_url(self, configuration_helpers_mock):
774+
"""Instructor dashboard gets SUPPORT_URL from help-tokens when no explicit override is set."""
775+
def side_effect(key, default=None):
776+
if key == "MFE_CONFIG":
777+
return {"LMS_BASE_URL": "https://courses.example.com"}
778+
if key == "MFE_CONFIG_OVERRIDES":
779+
return {}
780+
return default
781+
configuration_helpers_mock.get_value.side_effect = side_effect
782+
783+
response = self.client.get(self.url)
784+
data = response.json()
785+
786+
apps_by_id = {app["appId"]: app for app in data["apps"]}
787+
instructor = apps_by_id["org.openedx.frontend.app.instructorDashboard"]
788+
self.assertEqual( # noqa: PT009
789+
instructor["config"]["SUPPORT_URL"],
790+
"https://docs.openedx.org/en/latest/educators/index.html",
791+
)
792+
793+
@patch("lms.djangoapps.mfe_config_api.views.configuration_helpers")
794+
def test_explicit_override_wins_over_legacy_overrides(self, configuration_helpers_mock):
795+
"""An explicit SUPPORT_URL in MFE_CONFIG_OVERRIDES wins over the help-tokens fallback."""
796+
def side_effect(key, default=None):
797+
if key == "MFE_CONFIG":
798+
return {"LMS_BASE_URL": "https://courses.example.com"}
799+
if key == "MFE_CONFIG_OVERRIDES":
800+
return {
801+
"instructor-dashboard": {
802+
"SUPPORT_URL": "https://help.example.com/instructor",
803+
},
804+
}
805+
return default
806+
configuration_helpers_mock.get_value.side_effect = side_effect
807+
808+
response = self.client.get(self.url)
809+
data = response.json()
810+
811+
apps_by_id = {app["appId"]: app for app in data["apps"]}
812+
instructor = apps_by_id["org.openedx.frontend.app.instructorDashboard"]
813+
self.assertEqual( # noqa: PT009
814+
instructor["config"]["SUPPORT_URL"],
815+
"https://help.example.com/instructor",
816+
)

lms/djangoapps/mfe_config_api/views.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
MFE API Views for useful information related to mfes.
33
"""
44

5+
from configparser import Error as ConfigParserError
6+
57
import edx_api_doc_tools as apidocs
68
from django.conf import settings
79
from django.http import HttpResponseNotFound, JsonResponse
810
from django.utils.decorators import method_decorator
911
from django.views.decorators.cache import cache_page
12+
from help_tokens.core import HelpUrlExpert
1013
from rest_framework import status
1114
from rest_framework.exceptions import NotFound
1215
from rest_framework.permissions import AllowAny
@@ -48,6 +51,7 @@
4851
"course-authoring": "org.openedx.frontend.app.authoring",
4952
"discussions": "org.openedx.frontend.app.discussions",
5053
"gradebook": "org.openedx.frontend.app.gradebook",
54+
"instructor-dashboard": "org.openedx.frontend.app.instructorDashboard",
5155
"learner-dashboard": "org.openedx.frontend.app.learnerDashboard",
5256
"learner-record": "org.openedx.frontend.app.learnerRecord",
5357
"learning": "org.openedx.frontend.app.learning",
@@ -93,30 +97,76 @@ def get_mfe_config() -> dict:
9397
return mfe_config
9498

9599

96-
def get_mfe_config_overrides() -> dict:
97-
"""Return all MFE-specific overrides from settings or site configuration.
100+
def resolve_help_token(token: str) -> str | None:
101+
"""Resolve a help-tokens token to a URL, returning None if the token cannot be resolved."""
102+
try:
103+
return HelpUrlExpert.the_one().url_for_token(token)
104+
except (KeyError, ConfigParserError):
105+
return None
106+
107+
108+
def get_legacy_config_overrides() -> dict:
109+
"""Return per-app legacy configuration overrides.
110+
111+
Same shape as get_explicit_mfe_config_overrides(): a dict keyed by MFE name,
112+
where each value is a dict of config values.
113+
114+
This is a compatibility layer for per-app values that historically
115+
came from legacy systems (e.g., help-tokens).
116+
"""
117+
overrides: dict[str, dict] = {}
118+
119+
instructor_help_url = resolve_help_token("instructor")
120+
if instructor_help_url:
121+
overrides["instructor-dashboard"] = {"SUPPORT_URL": instructor_help_url}
122+
123+
return overrides
124+
125+
126+
def get_explicit_mfe_config_overrides() -> dict:
127+
"""Return MFE-specific overrides from settings or site configuration.
98128
99129
Returns:
100130
A dictionary keyed by MFE name, where each value is a dict of
101131
per-MFE overrides. Non-dict entries are filtered out.
102132
"""
103-
mfe_config_overrides = (
133+
raw_overrides = (
104134
configuration_helpers.get_value(
105135
"MFE_CONFIG_OVERRIDES",
106136
settings.MFE_CONFIG_OVERRIDES,
107137
)
108138
or {}
109139
)
110-
if not isinstance(mfe_config_overrides, dict):
140+
if not isinstance(raw_overrides, dict):
111141
return {}
112142

113143
return {
114-
name: overrides
115-
for name, overrides in mfe_config_overrides.items()
144+
mfe_name: overrides
145+
for mfe_name, overrides in raw_overrides.items()
116146
if isinstance(overrides, dict)
117147
}
118148

119149

150+
def get_mfe_config_overrides() -> dict:
151+
"""Return all MFE-specific overrides, merging legacy fallbacks with explicit settings.
152+
153+
Legacy per-app fallbacks (e.g., from help-tokens) are included at the lowest
154+
precedence; explicit MFE_CONFIG_OVERRIDES from settings or site configuration
155+
take priority.
156+
157+
Returns:
158+
A dictionary keyed by MFE name, where each value is a dict of
159+
per-MFE overrides.
160+
"""
161+
legacy_overrides = get_legacy_config_overrides()
162+
explicit_overrides = get_explicit_mfe_config_overrides()
163+
all_mfe_names = set(legacy_overrides) | set(explicit_overrides)
164+
return {
165+
mfe_name: legacy_overrides.get(mfe_name, {}) | explicit_overrides.get(mfe_name, {})
166+
for mfe_name in all_mfe_names
167+
}
168+
169+
120170
def get_frontend_site_config() -> dict:
121171
"""Return frontend site configuration from settings or site configuration.
122172

0 commit comments

Comments
 (0)