Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,6 @@

MARKETING_EMAILS_OPT_IN = False

############################# MICROFRONTENDS ###################################
COURSE_AUTHORING_MICROFRONTEND_URL = None

############################# SET PATH INFORMATION #############################
PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /edx-platform/cms
CMS_ROOT = REPO_ROOT / "cms" # noqa: F405
Expand Down
100 changes: 67 additions & 33 deletions lms/djangoapps/instructor/tests/test_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def setUp(self):
self.admin = AdminFactory.create()
self.instructor = InstructorFactory.create(course_key=self.course_key)
self.staff = StaffFactory.create(course_key=self.course_key)
self.django_staff_user = UserFactory.create(is_staff=True)
self.data_researcher = UserFactory.create()
CourseDataResearcherRole(self.course_key).add_users(self.data_researcher)
CourseInstructorRole(self.proctored_course.id).add_users(self.instructor)
Expand Down Expand Up @@ -118,60 +119,93 @@ def _get_url(self, course_id=None):
course_id = str(self.course_key)
return reverse('instructor_api_v2:course_metadata', kwargs={'course_id': course_id})

@override_settings(COURSE_AUTHORING_MICROFRONTEND_URL='http://localhost:2001/authoring')
@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
def test_get_course_metadata_as_instructor(self):
"""
Test that an instructor can retrieve comprehensive course metadata.
"""
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self._get_url())

self.assertEqual(response.status_code, status.HTTP_200_OK) # noqa: PT009
assert response.status_code == status.HTTP_200_OK
data = response.data

# Verify basic course information
self.assertEqual(data['course_id'], str(self.course_key)) # noqa: PT009
self.assertEqual(data['display_name'], 'Demonstration Course') # noqa: PT009
self.assertEqual(data['org'], 'edX') # noqa: PT009
self.assertEqual(data['course_number'], 'DemoX') # noqa: PT009
self.assertEqual(data['course_run'], 'Demo_Course') # noqa: PT009
self.assertEqual(data['pacing'], 'instructor') # noqa: PT009
assert data['course_id'] == str(self.course_key)
assert data['display_name'] == 'Demonstration Course'
assert data['org'] == 'edX'
assert data['course_number'] == 'DemoX'
assert data['course_run'] == 'Demo_Course'
assert data['pacing'] == 'instructor'

# Verify enrollment counts structure
self.assertIn('enrollment_counts', data) # noqa: PT009
self.assertIn('total', data['enrollment_counts']) # noqa: PT009
self.assertIn('total_enrollment', data) # noqa: PT009
self.assertGreaterEqual(data['total_enrollment'], 3) # noqa: PT009
assert 'enrollment_counts' in data
assert 'total' in data['enrollment_counts']
assert 'total_enrollment' in data
assert data['total_enrollment'] >= 3

# Verify role-based enrollment counts are present
self.assertIn('learner_count', data) # noqa: PT009
self.assertIn('staff_count', data) # noqa: PT009
self.assertEqual(data['total_enrollment'], data['learner_count'] + data['staff_count']) # noqa: PT009
assert 'learner_count' in data
assert 'staff_count' in data
assert data['total_enrollment'] == data['learner_count'] + data['staff_count']

# Verify permissions structure
self.assertIn('permissions', data) # noqa: PT009
assert 'permissions' in data
permissions_data = data['permissions']
self.assertIn('admin', permissions_data) # noqa: PT009
self.assertIn('instructor', permissions_data) # noqa: PT009
self.assertIn('staff', permissions_data) # noqa: PT009
self.assertIn('forum_admin', permissions_data) # noqa: PT009
self.assertIn('finance_admin', permissions_data) # noqa: PT009
self.assertIn('sales_admin', permissions_data) # noqa: PT009
self.assertIn('data_researcher', permissions_data) # noqa: PT009
assert 'admin' in permissions_data
assert 'instructor' in permissions_data
assert 'staff' in permissions_data
assert 'forum_admin' in permissions_data
assert 'finance_admin' in permissions_data
assert 'sales_admin' in permissions_data
assert 'data_researcher' in permissions_data

# Verify sections structure
self.assertIn('tabs', data) # noqa: PT009
self.assertIsInstance(data['tabs'], list) # noqa: PT009
assert 'tabs' in data
assert isinstance(data['tabs'], list)

# Verify other metadata fields
self.assertIn('num_sections', data) # noqa: PT009
self.assertIn('tabs', data) # noqa: PT009
self.assertIn('grade_cutoffs', data) # noqa: PT009
self.assertIn('course_errors', data) # noqa: PT009
self.assertIn('studio_url', data) # noqa: PT009
self.assertIn('disable_buttons', data) # noqa: PT009
self.assertIn('has_started', data) # noqa: PT009
self.assertIn('has_ended', data) # noqa: PT009
self.assertIn('analytics_dashboard_message', data) # noqa: PT009
assert 'num_sections' in data
assert 'grade_cutoffs' in data
assert 'course_errors' in data
assert 'studio_url' in data
assert 'disable_buttons' in data
assert 'has_started' in data
assert 'has_ended' in data
assert 'analytics_dashboard_message' in data
assert 'studio_grading_url' in data
assert 'admin_console_url' in data

assert data['studio_grading_url'] == f'http://localhost:2001/authoring/course/{self.course.id}/settings/grading'
assert data['admin_console_url'] == 'http://localhost:2025/admin-console/authz'

@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
def test_admin_console_url_requires_instructor_access(self):
"""
Test that the admin console URL is only available to users with instructor access.
"""
# data researcher has access to course but is not an instructor
self.client.force_authenticate(user=self.data_researcher)
response = self.client.get(self._get_url())

assert response.status_code == status.HTTP_200_OK
assert 'admin_console_url' in response.data
data = response.data
assert data['admin_console_url'] is None
Comment thread
dwong2708 marked this conversation as resolved.

@override_settings(ADMIN_CONSOLE_MICROFRONTEND_URL='http://localhost:2025/admin-console')
def test_django_staff_user_without_instructor_access_can_see_admin_console_url(self):
"""
Test that Django staff users without instructor access can see the admin console URL.
"""
self.client.force_authenticate(user=self.django_staff_user)
response = self.client.get(self._get_url())

assert response.status_code == status.HTTP_200_OK
assert 'admin_console_url' in response.data
data = response.data
assert data['admin_console_url'] == 'http://localhost:2025/admin-console/authz'

def test_get_course_metadata_as_staff(self):
"""
Expand Down
2 changes: 2 additions & 0 deletions lms/djangoapps/instructor/views/api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ def get(self, request, course_id):
"grade_cutoffs": "A is 0.9, B is 0.8, C is 0.7, D is 0.6",
"course_errors": [],
"studio_url": "https://studio.example.com/course/course-v1:edX+DemoX+2024",
# May be null if user does not have access:
"admin_console_url": "http://apps.local.openedx.io:2025/admin-console/authz",
"permissions": {
"admin": false,
"instructor": true,
Expand Down
29 changes: 25 additions & 4 deletions lms/djangoapps/instructor/views/serializers_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from lms.djangoapps.instructor.access import FORUM_ROLES, ROLES
from lms.djangoapps.instructor.views.instructor_dashboard import get_analytics_dashboard_message
from openedx.core.djangoapps.django_comment_common.models import FORUM_ROLE_ADMINISTRATOR
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from xmodule.modulestore.django import modulestore

from .tools import DashboardError, get_student_from_identifier, parse_datetime
Expand Down Expand Up @@ -77,6 +78,9 @@ class CourseInformationSerializerV2(serializers.Serializer):
studio_grading_url = serializers.SerializerMethodField(
help_text="URL to the Studio grading settings page for the course (null if not configured)"
)
admin_console_url = serializers.SerializerMethodField(
help_text="URL to the admin console (requires instructor access and MFE configuration, null if not accessible)"
)
permissions = serializers.SerializerMethodField(help_text="User permissions for instructor dashboard features")
tabs = serializers.SerializerMethodField(help_text="List of course tabs with configuration and display information")
disable_buttons = serializers.SerializerMethodField(
Expand Down Expand Up @@ -462,10 +466,27 @@ def get_gradebook_url(self, data):
def get_studio_grading_url(self, data):
"""Get Studio MFE grading settings URL for the course."""
course_key = data['course'].id
mfe_base_url = getattr(settings, 'COURSE_AUTHORING_MICROFRONTEND_URL', None)
if mfe_base_url:
return f'{mfe_base_url}/course/{course_key}/settings/grading'
return None
mfe_base_url = configuration_helpers.get_value(
'COURSE_AUTHORING_MICROFRONTEND_URL',
getattr(settings, 'COURSE_AUTHORING_MICROFRONTEND_URL', None)
)
if not mfe_base_url:
return None
return f'{mfe_base_url}/course/{course_key}/settings/grading'

def get_admin_console_url(self, data):
"""Get admin console URL (requires instructor access and MFE configuration, null if not accessible)."""
request = data['request']
has_instructor_access = has_access(request.user, 'instructor', data['course'])
mfe_base_url = configuration_helpers.get_value(
'ADMIN_CONSOLE_MICROFRONTEND_URL',
getattr(settings, 'ADMIN_CONSOLE_MICROFRONTEND_URL', None)
Comment on lines +481 to +483
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration_helpers.get_value usage here looks right, but ADMIN_CONSOLE_MICROFRONTEND_URL doesn't appear to be defined in any edx-platform settings module currently — the getattr fallback to None keeps it safe, but deployers won't know this setting exists without reading the serializer code.

I think we should add it to openedx/envs/common.py (defaulting to None) with the standard annotation block (setting_name, setting_description, setting_creation_date, setting_use_cases, etc.). The admin console MFE is pretty CMS-centric, and LMS needs the URL now — putting it in the shared settings avoids a future move or duplication if CMS needs it too. This would follow the same pattern as the other MFE URL settings.

)

has_permissions = request.user.is_staff or has_instructor_access
if not mfe_base_url or not has_permissions:
return None
return f'{mfe_base_url}/authz'

def get_disable_buttons(self, data):
"""Check if buttons should be disabled for large courses."""
Expand Down
1 change: 1 addition & 0 deletions lms/envs/devstack.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing
EXAMS_DASHBOARD_MICROFRONTEND_URL = 'http://localhost:2020'
INSTRUCTOR_MICROFRONTEND_URL = 'http://localhost:2003/instructor-dashboard'
CATALOG_MICROFRONTEND_URL = 'http://localhost:1998/catalog'
COURSE_AUTHORING_MICROFRONTEND_URL = 'http://localhost:2001/authoring'

################### FRONTEND APPLICATION DISCUSSIONS ###################
DISCUSSIONS_MICROFRONTEND_URL = 'http://localhost:2002'
Expand Down
14 changes: 14 additions & 0 deletions openedx/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1905,6 +1905,20 @@ def add_optional_apps(optional_apps, installed_apps):
# .. setting_description: Base URL of the micro-frontend-based learner home page.
LEARNER_HOME_MICROFRONTEND_URL = None

# .. setting_name: COURSE_AUTHORING_MICROFRONTEND_URL
# .. setting_default: None
# .. setting_description: Base URL of the micro-frontend-based course authoring (Studio) page.
# Used by both LMS and CMS backend code to construct links into the authoring MFE.
COURSE_AUTHORING_MICROFRONTEND_URL = None

# .. setting_name: ADMIN_CONSOLE_MICROFRONTEND_URL
# .. setting_default: None
# .. setting_description: Base URL of the micro-frontend-based admin console page.
# Used by LMS backend code to construct links into the admin console MFE.
# Although the admin console is CMS-centric, this setting lives in the shared
# base so both LMS and CMS can reference it without duplication.
ADMIN_CONSOLE_MICROFRONTEND_URL = None

################################## Swift ###################################

SWIFT_USERNAME = None
Expand Down
Loading