diff --git a/lms/djangoapps/instructor/docs/references/instructor-v2-course-info-spec.yaml b/lms/djangoapps/instructor/docs/references/instructor-v2-course-info-spec.yaml index 4b1ee72fcc..efea1f6f5c 100644 --- a/lms/djangoapps/instructor/docs/references/instructor-v2-course-info-spec.yaml +++ b/lms/djangoapps/instructor/docs/references/instructor-v2-course-info-spec.yaml @@ -254,6 +254,16 @@ definitions: minimum: 0 description: Total number of enrollments across all modes example: 150 + learner_count: + type: integer + minimum: 0 + description: Number of enrolled learners (excludes staff and admins) + example: 140 + staff_count: + type: integer + minimum: 0 + description: Number of enrolled staff and admins + example: 10 enrollment_counts: type: object description: | diff --git a/lms/djangoapps/instructor/tests/test_api_v2.py b/lms/djangoapps/instructor/tests/test_api_v2.py index d1320dd6c1..79e55dfdc6 100644 --- a/lms/djangoapps/instructor/tests/test_api_v2.py +++ b/lms/djangoapps/instructor/tests/test_api_v2.py @@ -125,6 +125,11 @@ class CourseMetadataViewTest(SharedModuleStoreTestCase): self.assertIn('total_enrollment', data) self.assertGreaterEqual(data['total_enrollment'], 3) + # Verify role-based enrollment counts are present + self.assertIn('learner_count', data) + self.assertIn('staff_count', data) + self.assertEqual(data['total_enrollment'], data['learner_count'] + data['staff_count']) + # Verify permissions structure self.assertIn('permissions', data) permissions_data = data['permissions'] @@ -217,6 +222,28 @@ class CourseMetadataViewTest(SharedModuleStoreTestCase): # Instructor should have instructor permission self.assertTrue(permissions_data['instructor']) + def test_learner_and_staff_counts(self): + """ + Test that learner_count excludes staff/admins and staff_count is the difference. + """ + self.client.force_authenticate(user=self.instructor) + response = self.client.get(self._get_url()) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + data = response.data + + total = data['total_enrollment'] + learner_count = data['learner_count'] + staff_count = data['staff_count'] + + # Counts must be non-negative and sum to total + self.assertGreaterEqual(learner_count, 0) + self.assertGreaterEqual(staff_count, 0) + self.assertEqual(total, learner_count + staff_count) + + # The student enrolled in setUp is not staff, so learner_count >= 1 + self.assertGreaterEqual(learner_count, 1) + def test_enrollment_counts_by_mode(self): """ Test that enrollment counts include all configured modes, diff --git a/lms/djangoapps/instructor/views/serializers_v2.py b/lms/djangoapps/instructor/views/serializers_v2.py index 185ff7fafe..6251dc5c50 100644 --- a/lms/djangoapps/instructor/views/serializers_v2.py +++ b/lms/djangoapps/instructor/views/serializers_v2.py @@ -54,6 +54,10 @@ class CourseInformationSerializerV2(serializers.Serializer): has_started = serializers.SerializerMethodField(help_text="Whether the course has started based on current time") has_ended = serializers.SerializerMethodField(help_text="Whether the course has ended based on current time") total_enrollment = serializers.SerializerMethodField(help_text="Total number of enrollments across all modes") + learner_count = serializers.SerializerMethodField( + help_text="Number of enrolled learners (excludes staff and admins)" + ) + staff_count = serializers.SerializerMethodField(help_text="Number of enrolled staff and admins") enrollment_counts = serializers.SerializerMethodField(help_text="Enrollment count breakdown by mode") num_sections = serializers.SerializerMethodField(help_text="Number of sections/chapters in the course") grade_cutoffs = serializers.SerializerMethodField(help_text="Formatted string of grade cutoffs") @@ -268,6 +272,14 @@ class CourseInformationSerializerV2(serializers.Serializer): """Get total enrollment count.""" return self.get_enrollment_counts(data)['total'] + def get_learner_count(self, data): + """Get enrollment count excluding staff and admins.""" + return CourseEnrollment.objects.num_enrolled_in_exclude_admins(data['course'].id) + + def get_staff_count(self, data): + """Get enrollment count for staff and admins only.""" + return self.get_total_enrollment(data) - self.get_learner_count(data) + def get_enrollment_counts(self, data): """Get enrollment counts for all configured course modes.""" course_id = data['course'].id