Skip to content

Commit 456a2db

Browse files
refactor: set setting map dynamically instead of hardcoded values
1 parent c5c9da0 commit 456a2db

File tree

6 files changed

+178
-11
lines changed

6 files changed

+178
-11
lines changed

cms/djangoapps/contentstore/rest_api/v0/tests/test_advanced_settings.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rest_framework.exceptions import ValidationError
1212

1313
from cms.djangoapps.contentstore.tests.utils import CourseTestCase
14+
from openedx.core.djangoapps.course_apps.plugins import CourseAppsPluginManager
1415

1516

1617
@ddt.ddt
@@ -216,3 +217,49 @@ def test_patch_advanced_setting_with_none_value(self, mock_set_course_app_status
216217
# Verify set_course_app_status was not called when value is None
217218
mock_set_course_app_status.assert_not_called()
218219
assert response.status_code == 200
220+
221+
def test_dynamic_course_app_settings_mapping(self):
222+
"""
223+
Test that the course app settings mapping is dynamically discovered from plugins.
224+
"""
225+
# Get the dynamic mapping
226+
mapping = CourseAppsPluginManager.get_course_app_settings_mapping()
227+
228+
# Verify that calculator and edxnotes are in the mapping
229+
assert "show_calculator" in mapping
230+
assert mapping["show_calculator"] == "calculator"
231+
assert "edxnotes" in mapping
232+
assert mapping["edxnotes"] == "edxnotes"
233+
234+
@patch(
235+
'cms.djangoapps.contentstore.rest_api.v0.views.advanced_settings.'
236+
'CourseAppsPluginManager.get_course_app_settings_mapping'
237+
)
238+
@patch('cms.djangoapps.contentstore.rest_api.v0.views.advanced_settings.set_course_app_status')
239+
def test_patch_uses_dynamic_mapping(self, mock_set_course_app_status, mock_get_mapping):
240+
"""
241+
Test that PATCH uses the dynamic mapping instead of hardcoded values.
242+
"""
243+
# Mock the dynamic mapping to return custom mapping
244+
mock_get_mapping.return_value = {
245+
"show_calculator": "calculator",
246+
"custom_app_setting": "custom_app"
247+
}
248+
mock_set_course_app_status.return_value = True
249+
250+
data = {
251+
"custom_app_setting": {
252+
"value": True
253+
}
254+
}
255+
response = self.client.patch(self.url, json.dumps(data), content_type="application/json")
256+
257+
# Verify the dynamic mapping was called
258+
mock_get_mapping.assert_called_once()
259+
260+
# Verify set_course_app_status was called with the custom app
261+
mock_set_course_app_status.assert_called_once()
262+
call_args = mock_set_course_app_status.call_args.kwargs
263+
assert call_args['app_id'] == 'custom_app'
264+
assert call_args['enabled'] is True
265+
assert response.status_code == 200

cms/djangoapps/contentstore/rest_api/v0/views/advanced_settings.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from cms.djangoapps.contentstore.api.views.utils import get_bool_param
1414
from common.djangoapps.student.auth import has_studio_read_access, has_studio_write_access
1515
from openedx.core.djangoapps.course_apps.api import set_course_app_status
16+
from openedx.core.djangoapps.course_apps.plugins import CourseAppsPluginManager
1617
from openedx.core.lib.api.view_utils import DeveloperErrorViewMixin, verify_course_exists, view_auth_classes
1718
from ..serializers import CourseAdvancedSettingsSerializer
1819
from ....views.course import update_course_advanced_settings
@@ -144,6 +145,7 @@ def get(self, request: Request, course_id: str):
144145
def patch(self, request: Request, course_id: str):
145146
"""
146147
Update a course's advanced settings.
148+
Also update the status of the course apps that have corresponding advanced settings fields.
147149
148150
**Example Request**
149151
@@ -188,12 +190,7 @@ def patch(self, request: Request, course_id: str):
188190
if not has_studio_write_access(request.user, course_key):
189191
self.permission_denied(request)
190192

191-
# These settings correspond to course apps
192-
# The keys are the advanced setting names, and the values are the corresponding app IDs
193-
course_app_settings_map = {
194-
"show_calculator": "calculator",
195-
"edxnotes": "edxnotes",
196-
}
193+
course_app_settings_map = CourseAppsPluginManager.get_course_app_settings_mapping()
197194
for setting in course_app_settings_map:
198195
if setting_to_update := request.data.get(setting):
199196
course_app_enabled = setting_to_update.get("value", None)
@@ -209,7 +206,8 @@ def patch(self, request: Request, course_id: str):
209206
# the advanced settings, so we remove it from the request data
210207
request.data.pop(setting)
211208
except ValidationError:
212-
# Ignore errors and let the normal flow handle updates
209+
# Failed to update the course app status,
210+
# we ignore and let the normal flow handle updates
213211
pass
214212

215213
course_block = modulestore().get_course(course_key)

lms/djangoapps/courseware/plugins.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ class CalculatorCourseApp(CourseApp):
127127
documentation_links = {
128128
"learn_more_configuration": settings.CALCULATOR_HELP_URL,
129129
}
130+
advanced_setting_field = "show_calculator"
130131

131132
@classmethod
132133
def is_available(cls, course_key: CourseKey) -> bool:
@@ -140,15 +141,15 @@ def is_enabled(cls, course_key: CourseKey) -> bool:
140141
"""
141142
Get calculator enabled status from course overview model.
142143
"""
143-
return CourseOverview.get_from_id(course_key).show_calculator
144+
return getattr(CourseOverview.get_from_id(course_key), cls.advanced_setting_field, False)
144145

145146
@classmethod
146147
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: 'User') -> bool:
147148
"""
148149
Update calculator enabled status in modulestore.
149150
"""
150151
course = get_course_by_id(course_key)
151-
course.show_calculator = enabled
152+
setattr(course, cls.advanced_setting_field, enabled)
152153
modulestore().update_item(course, user.id)
153154
return enabled
154155

lms/djangoapps/edxnotes/plugins.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class EdxNotesCourseApp(CourseApp):
5959
documentation_links = {
6060
"learn_more_configuration": settings.EDXNOTES_HELP_URL,
6161
}
62+
advanced_setting_field = "edxnotes"
6263

6364
@classmethod
6465
def is_available(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-argument
@@ -72,15 +73,15 @@ def is_enabled(cls, course_key: CourseKey) -> bool: # pylint: disable=unused-ar
7273
"""
7374
Get enabled/disabled status from modulestore.
7475
"""
75-
return CourseOverview.get_from_id(course_key).edxnotes
76+
return getattr(CourseOverview.get_from_id(course_key), cls.advanced_setting_field, False)
7677

7778
@classmethod
7879
def set_enabled(cls, course_key: CourseKey, enabled: bool, user: 'User') -> bool:
7980
"""
8081
Enable/disable edxnotes in the modulestore.
8182
"""
8283
course = get_course_by_id(course_key)
83-
course.edxnotes = enabled
84+
setattr(course, cls.advanced_setting_field, enabled)
8485
if enabled:
8586
notes_tab = CourseTabList.get_tab_by_id(course.tabs, 'edxnotes')
8687
if notes_tab is None:

openedx/core/djangoapps/course_apps/plugins.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class CourseApp(ABC):
2828
# eg:
2929
# "learn_more_configuration": "https://..."
3030
}
31+
# Advanced setting field name that corresponds to this app (optional)
32+
# Should be a string representing the field name in course advanced settings
33+
advanced_setting_field = None
3134

3235
@classmethod
3336
@abstractmethod
@@ -115,3 +118,22 @@ def get_apps_available_for_course(cls, course_key: CourseKey) -> Iterator[Course
115118
for plugin in super().get_available_plugins().values():
116119
if plugin.is_available(course_key):
117120
yield plugin
121+
122+
@classmethod
123+
def get_course_app_settings_mapping(cls) -> Dict[str, str]:
124+
"""
125+
Returns a mapping of advanced setting field names to course app IDs.
126+
127+
This method dynamically discovers all course app plugins and builds a mapping
128+
based on their advanced_setting_field attributes.
129+
130+
Returns:
131+
Dict[str, str]: A dictionary mapping advanced setting field names to app IDs.
132+
"""
133+
settings_mapping = {}
134+
135+
for plugin in super().get_available_plugins().values():
136+
if hasattr(plugin, 'advanced_setting_field') and plugin.advanced_setting_field:
137+
settings_mapping[plugin.advanced_setting_field] = plugin.app_id
138+
139+
return settings_mapping
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Tests for dynamic course app settings mapping functionality.
3+
"""
4+
from unittest.mock import patch
5+
6+
from django.test import TestCase
7+
8+
from openedx.core.djangoapps.course_apps.plugins import CourseAppsPluginManager, CourseApp
9+
10+
11+
class MockCourseAppWithSetting(CourseApp):
12+
"""Mock course app with an advanced setting field."""
13+
14+
app_id = "settings_app"
15+
name = "Settings App"
16+
description = "An app with advanced settings"
17+
advanced_setting_field = "settings_app_field"
18+
19+
@classmethod
20+
def is_available(cls, course_key):
21+
return True
22+
23+
@classmethod
24+
def is_enabled(cls, course_key):
25+
return True
26+
27+
@classmethod
28+
def set_enabled(cls, course_key, enabled, user):
29+
return enabled
30+
31+
@classmethod
32+
def get_allowed_operations(cls, course_key, user=None):
33+
return {"enable": True, "configure": True}
34+
35+
36+
class MockCourseAppNoSettings(CourseApp):
37+
"""Mock course app with no advanced settings mapping."""
38+
39+
app_id = "no_settings_app"
40+
name = "No Settings App"
41+
description = "An app without advanced settings"
42+
43+
@classmethod
44+
def is_available(cls, course_key):
45+
return True
46+
47+
@classmethod
48+
def is_enabled(cls, course_key):
49+
return True
50+
51+
@classmethod
52+
def set_enabled(cls, course_key, enabled, user):
53+
return enabled
54+
55+
@classmethod
56+
def get_allowed_operations(cls, course_key, user=None):
57+
return {"enable": True, "configure": True}
58+
59+
60+
class DynamicSettingsMappingTest(TestCase):
61+
"""Test dynamic course app settings mapping functionality."""
62+
63+
def test_app_with_advanced_setting_mapping(self):
64+
"""Test that a course app with an advanced setting field is mapped correctly."""
65+
mock_plugins = {
66+
"settings_app": MockCourseAppWithSetting(),
67+
}
68+
69+
with patch('edx_django_utils.plugins.PluginManager.get_available_plugins', return_value=mock_plugins):
70+
mapping = CourseAppsPluginManager.get_course_app_settings_mapping()
71+
72+
assert "settings_app_field" in mapping
73+
assert mapping["settings_app_field"] == "settings_app"
74+
75+
def test_no_advanced_setting_fields(self):
76+
"""Test that a course app without advanced_setting_fields is not included in mapping."""
77+
mock_plugins = {
78+
"no_settings_app": MockCourseAppNoSettings(),
79+
}
80+
81+
with patch('edx_django_utils.plugins.PluginManager.get_available_plugins', return_value=mock_plugins):
82+
mapping = CourseAppsPluginManager.get_course_app_settings_mapping()
83+
84+
assert len(mapping) == 0
85+
86+
def test_mixed_apps_mapping(self):
87+
"""Test mapping with a mix of apps with and without advanced settings."""
88+
mock_plugins = {
89+
"settings_app": MockCourseAppWithSetting(),
90+
"no_settings_app": MockCourseAppNoSettings(),
91+
}
92+
93+
with patch('edx_django_utils.plugins.PluginManager.get_available_plugins', return_value=mock_plugins):
94+
mapping = CourseAppsPluginManager.get_course_app_settings_mapping()
95+
96+
# Should only include apps with advanced_setting_field
97+
assert len(mapping) == 1
98+
assert mapping["settings_app_field"] == "settings_app"

0 commit comments

Comments
 (0)