Skip to content

Commit 66191a9

Browse files
authored
feat: add admin_console_url to instructor API v2 course metadata (#38415)
- Add admin_console_url field gated by instructor access or Django staff - Update studio_grading_url to use configuration_helpers for site-config override support - Move COURSE_AUTHORING_MICROFRONTEND_URL to shared settings - Add ADMIN_CONSOLE_MICROFRONTEND_URL to shared settings
1 parent 7dd54dc commit 66191a9

6 files changed

Lines changed: 106 additions & 40 deletions

File tree

cms/envs/common.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,6 @@
256256

257257
MARKETING_EMAILS_OPT_IN = False
258258

259-
############################# MICROFRONTENDS ###################################
260-
COURSE_AUTHORING_MICROFRONTEND_URL = None
261-
262259
############################# SET PATH INFORMATION #############################
263260
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/cms
264261
CMS_ROOT = REPO_ROOT / "cms" # noqa: F405

lms/djangoapps/instructor/tests/test_api_v2.py

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def setUp(self):
8181
self.admin = AdminFactory.create()
8282
self.instructor = InstructorFactory.create(course_key=self.course_key)
8383
self.staff = StaffFactory.create(course_key=self.course_key)
84+
self.django_staff_user = UserFactory.create(is_staff=True)
8485
self.data_researcher = UserFactory.create()
8586
CourseDataResearcherRole(self.course_key).add_users(self.data_researcher)
8687
CourseInstructorRole(self.proctored_course.id).add_users(self.instructor)
@@ -118,60 +119,93 @@ def _get_url(self, course_id=None):
118119
course_id = str(self.course_key)
119120
return reverse('instructor_api_v2:course_metadata', kwargs={'course_id': course_id})
120121

122+
@override_settings(COURSE_AUTHORING_MICROFRONTEND_URL='http://localhost:2001/authoring')
123+
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
121124
def test_get_course_metadata_as_instructor(self):
122125
"""
123126
Test that an instructor can retrieve comprehensive course metadata.
124127
"""
125128
self.client.force_authenticate(user=self.instructor)
126129
response = self.client.get(self._get_url())
127130

128-
self.assertEqual(response.status_code, status.HTTP_200_OK) # noqa: PT009
131+
assert response.status_code == status.HTTP_200_OK
129132
data = response.data
130133

131134
# Verify basic course information
132-
self.assertEqual(data['course_id'], str(self.course_key)) # noqa: PT009
133-
self.assertEqual(data['display_name'], 'Demonstration Course') # noqa: PT009
134-
self.assertEqual(data['org'], 'edX') # noqa: PT009
135-
self.assertEqual(data['course_number'], 'DemoX') # noqa: PT009
136-
self.assertEqual(data['course_run'], 'Demo_Course') # noqa: PT009
137-
self.assertEqual(data['pacing'], 'instructor') # noqa: PT009
135+
assert data['course_id'] == str(self.course_key)
136+
assert data['display_name'] == 'Demonstration Course'
137+
assert data['org'] == 'edX'
138+
assert data['course_number'] == 'DemoX'
139+
assert data['course_run'] == 'Demo_Course'
140+
assert data['pacing'] == 'instructor'
138141

139142
# Verify enrollment counts structure
140-
self.assertIn('enrollment_counts', data) # noqa: PT009
141-
self.assertIn('total', data['enrollment_counts']) # noqa: PT009
142-
self.assertIn('total_enrollment', data) # noqa: PT009
143-
self.assertGreaterEqual(data['total_enrollment'], 3) # noqa: PT009
143+
assert 'enrollment_counts' in data
144+
assert 'total' in data['enrollment_counts']
145+
assert 'total_enrollment' in data
146+
assert data['total_enrollment'] >= 3
144147

145148
# Verify role-based enrollment counts are present
146-
self.assertIn('learner_count', data) # noqa: PT009
147-
self.assertIn('staff_count', data) # noqa: PT009
148-
self.assertEqual(data['total_enrollment'], data['learner_count'] + data['staff_count']) # noqa: PT009
149+
assert 'learner_count' in data
150+
assert 'staff_count' in data
151+
assert data['total_enrollment'] == data['learner_count'] + data['staff_count']
149152

150153
# Verify permissions structure
151-
self.assertIn('permissions', data) # noqa: PT009
154+
assert 'permissions' in data
152155
permissions_data = data['permissions']
153-
self.assertIn('admin', permissions_data) # noqa: PT009
154-
self.assertIn('instructor', permissions_data) # noqa: PT009
155-
self.assertIn('staff', permissions_data) # noqa: PT009
156-
self.assertIn('forum_admin', permissions_data) # noqa: PT009
157-
self.assertIn('finance_admin', permissions_data) # noqa: PT009
158-
self.assertIn('sales_admin', permissions_data) # noqa: PT009
159-
self.assertIn('data_researcher', permissions_data) # noqa: PT009
156+
assert 'admin' in permissions_data
157+
assert 'instructor' in permissions_data
158+
assert 'staff' in permissions_data
159+
assert 'forum_admin' in permissions_data
160+
assert 'finance_admin' in permissions_data
161+
assert 'sales_admin' in permissions_data
162+
assert 'data_researcher' in permissions_data
160163

161164
# Verify sections structure
162-
self.assertIn('tabs', data) # noqa: PT009
163-
self.assertIsInstance(data['tabs'], list) # noqa: PT009
165+
assert 'tabs' in data
166+
assert isinstance(data['tabs'], list)
164167

165168
# Verify other metadata fields
166-
self.assertIn('num_sections', data) # noqa: PT009
167-
self.assertIn('tabs', data) # noqa: PT009
168-
self.assertIn('grade_cutoffs', data) # noqa: PT009
169-
self.assertIn('course_errors', data) # noqa: PT009
170-
self.assertIn('studio_url', data) # noqa: PT009
171-
self.assertIn('disable_buttons', data) # noqa: PT009
172-
self.assertIn('has_started', data) # noqa: PT009
173-
self.assertIn('has_ended', data) # noqa: PT009
174-
self.assertIn('analytics_dashboard_message', data) # noqa: PT009
169+
assert 'num_sections' in data
170+
assert 'grade_cutoffs' in data
171+
assert 'course_errors' in data
172+
assert 'studio_url' in data
173+
assert 'disable_buttons' in data
174+
assert 'has_started' in data
175+
assert 'has_ended' in data
176+
assert 'analytics_dashboard_message' in data
177+
assert 'studio_grading_url' in data
178+
assert 'admin_console_url' in data
179+
180+
assert data['studio_grading_url'] == f'http://localhost:2001/authoring/course/{self.course.id}/settings/grading'
181+
assert data['admin_console_url'] == 'http://localhost:2025/admin-console/authz'
182+
183+
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
184+
def test_admin_console_url_requires_instructor_access(self):
185+
"""
186+
Test that the admin console URL is only available to users with instructor access.
187+
"""
188+
# data researcher has access to course but is not an instructor
189+
self.client.force_authenticate(user=self.data_researcher)
190+
response = self.client.get(self._get_url())
191+
192+
assert response.status_code == status.HTTP_200_OK
193+
assert 'admin_console_url' in response.data
194+
data = response.data
195+
assert data['admin_console_url'] is None
196+
197+
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
198+
def test_django_staff_user_without_instructor_access_can_see_admin_console_url(self):
199+
"""
200+
Test that Django staff users without instructor access can see the admin console URL.
201+
"""
202+
self.client.force_authenticate(user=self.django_staff_user)
203+
response = self.client.get(self._get_url())
204+
205+
assert response.status_code == status.HTTP_200_OK
206+
assert 'admin_console_url' in response.data
207+
data = response.data
208+
assert data['admin_console_url'] == 'http://localhost:2025/admin-console/authz'
175209

176210
def test_get_course_metadata_as_staff(self):
177211
"""

lms/djangoapps/instructor/views/api_v2.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@ def get(self, request, course_id):
234234
"grade_cutoffs": "A is 0.9, B is 0.8, C is 0.7, D is 0.6",
235235
"course_errors": [],
236236
"studio_url": "https://studio.example.com/course/course-v1:edX+DemoX+2024",
237+
# May be null if user does not have access:
238+
"admin_console_url": "http://apps.local.openedx.io:2025/admin-console/authz",
237239
"permissions": {
238240
"admin": false,
239241
"instructor": true,

lms/djangoapps/instructor/views/serializers_v2.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from lms.djangoapps.instructor.access import FORUM_ROLES, ROLES
3535
from lms.djangoapps.instructor.views.instructor_dashboard import get_analytics_dashboard_message
3636
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
37+
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
3738
from xmodule.modulestore.django import modulestore
3839

3940
from .tools import DashboardError, get_student_from_identifier, parse_datetime
@@ -77,6 +78,9 @@ class CourseInformationSerializerV2(serializers.Serializer):
7778
studio_grading_url = serializers.SerializerMethodField(
7879
help_text="URL to the Studio grading settings page for the course (null if not configured)"
7980
)
81+
admin_console_url = serializers.SerializerMethodField(
82+
help_text="URL to the admin console (requires instructor access and MFE configuration, null if not accessible)"
83+
)
8084
permissions = serializers.SerializerMethodField(help_text="User permissions for instructor dashboard features")
8185
tabs = serializers.SerializerMethodField(help_text="List of course tabs with configuration and display information")
8286
disable_buttons = serializers.SerializerMethodField(
@@ -462,10 +466,27 @@ def get_gradebook_url(self, data):
462466
def get_studio_grading_url(self, data):
463467
"""Get Studio MFE grading settings URL for the course."""
464468
course_key = data['course'].id
465-
mfe_base_url = getattr(settings, 'COURSE_AUTHORING_MICROFRONTEND_URL', None)
466-
if mfe_base_url:
467-
return f'{mfe_base_url}/course/{course_key}/settings/grading'
468-
return None
469+
mfe_base_url = configuration_helpers.get_value(
470+
'COURSE_AUTHORING_MICROFRONTEND_URL',
471+
getattr(settings, 'COURSE_AUTHORING_MICROFRONTEND_URL', None)
472+
)
473+
if not mfe_base_url:
474+
return None
475+
return f'{mfe_base_url}/course/{course_key}/settings/grading'
476+
477+
def get_admin_console_url(self, data):
478+
"""Get admin console URL (requires instructor access and MFE configuration, null if not accessible)."""
479+
request = data['request']
480+
has_instructor_access = has_access(request.user, 'instructor', data['course'])
481+
mfe_base_url = configuration_helpers.get_value(
482+
'ADMIN_CONSOLE_MICROFRONTEND_URL',
483+
getattr(settings, 'ADMIN_CONSOLE_MICROFRONTEND_URL', None)
484+
)
485+
486+
has_permissions = request.user.is_staff or has_instructor_access
487+
if not mfe_base_url or not has_permissions:
488+
return None
489+
return f'{mfe_base_url}/authz'
469490

470491
def get_disable_buttons(self, data):
471492
"""Check if buttons should be disabled for large courses."""

lms/envs/devstack.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing
400400
EXAMS_DASHBOARD_MICROFRONTEND_URL = 'http://localhost:2020'
401401
INSTRUCTOR_MICROFRONTEND_URL = 'http://localhost:2003/instructor-dashboard'
402402
CATALOG_MICROFRONTEND_URL = 'http://localhost:1998/catalog'
403+
COURSE_AUTHORING_MICROFRONTEND_URL = 'http://localhost:2001/authoring'
404+
ADMIN_CONSOLE_MICROFRONTEND_URL = 'http://localhost:2025/admin-console'
403405

404406
################### FRONTEND APPLICATION DISCUSSIONS ###################
405407
DISCUSSIONS_MICROFRONTEND_URL = 'http://localhost:2002'

openedx/envs/common.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1905,6 +1905,16 @@ def add_optional_apps(optional_apps, installed_apps):
19051905
# .. setting_description: Base URL of the micro-frontend-based learner home page.
19061906
LEARNER_HOME_MICROFRONTEND_URL = None
19071907

1908+
# .. setting_name: COURSE_AUTHORING_MICROFRONTEND_URL
1909+
# .. setting_default: None
1910+
# .. setting_description: Base URL of the micro-frontend-based course authoring (Studio) page.
1911+
COURSE_AUTHORING_MICROFRONTEND_URL = None
1912+
1913+
# .. setting_name: ADMIN_CONSOLE_MICROFRONTEND_URL
1914+
# .. setting_default: None
1915+
# .. setting_description: Base URL of the micro-frontend-based admin console page.
1916+
ADMIN_CONSOLE_MICROFRONTEND_URL = None
1917+
19081918
################################## Swift ###################################
19091919

19101920
SWIFT_USERNAME = None

0 commit comments

Comments
 (0)