feat: Adds course_run field to CourseInformationSerializer v2
Adds sort_order field to tabs JSON in the tabs list in CourseInformationSerializer v2 serializer Move course_run below course_number and also move the serializer method. Auto-format removing extraneous white space. Add trailing commas to the instructor course tabs list to improve the diff going forward. Add comment about sort order
This commit is contained in:
@@ -110,6 +110,7 @@ class CourseMetadataViewTest(SharedModuleStoreTestCase):
|
||||
self.assertEqual(data['display_name'], 'Demonstration Course')
|
||||
self.assertEqual(data['org'], 'edX')
|
||||
self.assertEqual(data['course_number'], 'DemoX')
|
||||
self.assertEqual(data['course_run'], 'Demo_Course')
|
||||
self.assertEqual(data['pacing'], 'instructor')
|
||||
|
||||
# Verify enrollment counts structure
|
||||
@@ -346,6 +347,16 @@ class CourseMetadataViewTest(SharedModuleStoreTestCase):
|
||||
tab_ids = [tab['tab_id'] for tab in tabs]
|
||||
self.assertIn('certificates', tab_ids)
|
||||
|
||||
def test_tabs_have_sort_order(self):
|
||||
"""
|
||||
Test that all tabs include a sort_order field.
|
||||
"""
|
||||
tabs = self._get_tabs_from_response(self.staff)
|
||||
|
||||
for tab in tabs:
|
||||
self.assertIn('sort_order', tab)
|
||||
self.assertIsInstance(tab['sort_order'], int)
|
||||
|
||||
def test_disable_buttons_false_for_small_course(self):
|
||||
"""
|
||||
Test that disable_buttons is False for courses with <=200 enrollments.
|
||||
|
||||
@@ -135,6 +135,76 @@ class CourseMetadataView(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
Retrieve comprehensive course information including metadata, enrollment statistics,
|
||||
dashboard configuration, and user permissions.
|
||||
|
||||
**Use Cases**
|
||||
|
||||
Retrieve comprehensive course metadata including enrollment counts, dashboard configuration,
|
||||
permissions, and navigation sections.
|
||||
|
||||
**Example Requests**
|
||||
|
||||
GET /api/instructor/v2/courses/{course_id}
|
||||
|
||||
**Response Values**
|
||||
|
||||
{
|
||||
"course_id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"display_name": "Demonstration Course",
|
||||
"org": "edX",
|
||||
"course_number": "DemoX",
|
||||
"enrollment_start": "2013-02-05T00:00:00Z",
|
||||
"enrollment_end": null,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"end": "2024-12-31T23:59:59Z",
|
||||
"pacing": "instructor",
|
||||
"has_started": true,
|
||||
"has_ended": false,
|
||||
"total_enrollment": 150,
|
||||
"enrollment_counts": {
|
||||
"total": 150,
|
||||
"audit": 100,
|
||||
"verified": 40,
|
||||
"honor": 10
|
||||
},
|
||||
"num_sections": 12,
|
||||
"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",
|
||||
"permissions": {
|
||||
"admin": false,
|
||||
"instructor": true,
|
||||
"finance_admin": false,
|
||||
"sales_admin": false,
|
||||
"staff": true,
|
||||
"forum_admin": true,
|
||||
"data_researcher": false
|
||||
},
|
||||
"tabs": [
|
||||
{
|
||||
"tab_id": "courseware",
|
||||
"title": "Course",
|
||||
"url": "INSTRUCTOR_MICROFRONTEND_URL/courses/course-v1:edX+DemoX+2024/courseware"
|
||||
},
|
||||
{
|
||||
"tab_id": "progress",
|
||||
"title": "Progress",
|
||||
"url": "INSTRUCTOR_MICROFRONTEND_URL/courses/course-v1:edX+DemoX+2024/progress"
|
||||
},
|
||||
],
|
||||
"disable_buttons": false,
|
||||
"analytics_dashboard_message": "To gain insights into student enrollment and participation..."
|
||||
}
|
||||
|
||||
**Parameters**
|
||||
|
||||
course_key: Course key for the course.
|
||||
|
||||
**Returns**
|
||||
|
||||
* 200: OK - Returns course metadata
|
||||
* 401: Unauthorized - User is not authenticated
|
||||
* 403: Forbidden - User lacks instructor permissions
|
||||
* 404: Not Found - Course does not exist
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
course = get_course_by_id(course_key)
|
||||
|
||||
@@ -19,6 +19,7 @@ from common.djangoapps.student.roles import (
|
||||
CourseSalesAdminRole,
|
||||
CourseStaffRole,
|
||||
)
|
||||
from lms.djangoapps.bulk_email.api import is_bulk_email_feature_enabled
|
||||
from lms.djangoapps.certificates.models import (
|
||||
CertificateGenerationConfiguration
|
||||
)
|
||||
@@ -44,6 +45,7 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
display_name = serializers.SerializerMethodField(help_text="Course display name")
|
||||
org = serializers.SerializerMethodField(help_text="Organization identifier")
|
||||
course_number = serializers.SerializerMethodField(help_text="Course number")
|
||||
course_run = serializers.SerializerMethodField(help_text="Course run identifier")
|
||||
enrollment_start = serializers.SerializerMethodField(help_text="Enrollment start date (ISO 8601 with timezone)")
|
||||
enrollment_end = serializers.SerializerMethodField(help_text="Enrollment end date (ISO 8601 with timezone)")
|
||||
start = serializers.SerializerMethodField(help_text="Course start date (ISO 8601 with timezone)")
|
||||
@@ -83,47 +85,70 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
'data_researcher': request.user.has_perm(permissions.CAN_RESEARCH, course_key),
|
||||
}
|
||||
tabs = []
|
||||
|
||||
# NOTE: The Instructor experience can be extended via FE plugins that insert tabs
|
||||
# dynamically using explicit priority values. The sort_order field provides a stable
|
||||
# ordering contract so plugins created via the FE can reliably position themselves
|
||||
# relative to backend-defined tabs (e.g., "insert between Grading and Course Team").
|
||||
# Without explicit sort_order values, there's no deterministic way to interleave
|
||||
# backend tabs with plugin-inserted tabs, and tab order could shift based on
|
||||
# load/config timing.
|
||||
if access['staff']:
|
||||
tabs.extend([
|
||||
{
|
||||
'tab_id': 'course_info',
|
||||
'title': _('Course Info'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/course_info'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/course_info',
|
||||
'sort_order': 10,
|
||||
},
|
||||
{
|
||||
'tab_id': 'enrollments',
|
||||
'title': _('Enrollments'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/enrollments'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/enrollments',
|
||||
'sort_order': 20,
|
||||
},
|
||||
{
|
||||
"tab_id": "course_team",
|
||||
"title": "Course Team",
|
||||
"url": f'{mfe_base_url}/instructor/{str(course_key)}/course_team'
|
||||
"url": f'{mfe_base_url}/instructor/{str(course_key)}/course_team',
|
||||
'sort_order': 30,
|
||||
},
|
||||
{
|
||||
'tab_id': 'grading',
|
||||
'title': _('Grading'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/grading'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/grading',
|
||||
'sort_order': 40,
|
||||
},
|
||||
{
|
||||
'tab_id': 'cohorts',
|
||||
'title': _('Cohorts'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/cohorts'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/cohorts',
|
||||
'sort_order': 90,
|
||||
},
|
||||
])
|
||||
|
||||
if access['staff'] and is_bulk_email_feature_enabled(course_key):
|
||||
tabs.append({
|
||||
'tab_id': 'bulk_email',
|
||||
'title': _('Bulk Email'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/bulk_email',
|
||||
'sort_order': 100,
|
||||
})
|
||||
|
||||
if access['instructor'] and is_enabled_for_course(course_key):
|
||||
tabs.append({
|
||||
'tab_id': 'date_extensions',
|
||||
'title': _('Date Extensions'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/date_extensions'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/date_extensions',
|
||||
'sort_order': 50,
|
||||
})
|
||||
|
||||
if access['data_researcher']:
|
||||
tabs.append({
|
||||
'tab_id': 'data_downloads',
|
||||
'title': _('Data Downloads'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/data_downloads'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/data_downloads',
|
||||
'sort_order': 60,
|
||||
})
|
||||
|
||||
openassessment_blocks = modulestore().get_items(
|
||||
@@ -137,7 +162,8 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
tabs.append({
|
||||
'tab_id': 'open_responses',
|
||||
'title': _('Open Responses'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/open_responses'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/open_responses',
|
||||
'sort_order': 70,
|
||||
})
|
||||
|
||||
# Note: This is hidden for all CCXs
|
||||
@@ -148,7 +174,8 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
tabs.append({
|
||||
'tab_id': 'certificates',
|
||||
'title': _('Certificates'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/certificates'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/certificates',
|
||||
'sort_order': 80,
|
||||
})
|
||||
|
||||
user_has_access = any([
|
||||
@@ -164,7 +191,8 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
tabs.append({
|
||||
'tab_id': 'special_exams',
|
||||
'title': _('Special Exams'),
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/special_exams'
|
||||
'url': f'{mfe_base_url}/instructor/{str(course_key)}/special_exams',
|
||||
'sort_order': 110,
|
||||
})
|
||||
|
||||
# We provide the tabs in a specific order based on how it was
|
||||
@@ -202,6 +230,11 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
"""Get course number."""
|
||||
return data['course'].id.course
|
||||
|
||||
def get_course_run(self, data):
|
||||
"""Get course run identifier"""
|
||||
course_id = data['course'].id
|
||||
return course_id.run if course_id.run is not None else ''
|
||||
|
||||
def get_enrollment_start(self, data):
|
||||
"""Get enrollment start date."""
|
||||
return data['course'].enrollment_start
|
||||
@@ -323,6 +356,7 @@ class CourseInformationSerializer(serializers.Serializer):
|
||||
return get_analytics_dashboard_message(data['course'].id)
|
||||
|
||||
|
||||
|
||||
class InstructorTaskSerializer(serializers.Serializer):
|
||||
"""Serializer for instructor task details."""
|
||||
task_id = serializers.UUIDField()
|
||||
|
||||
Reference in New Issue
Block a user