fix: optimize enrollment counts to use read replica and show all configured modes (#38006)

This commit is contained in:
Brian Buck
2026-02-12 11:39:31 -07:00
committed by Taylor Payne
parent 8143796b26
commit 44521091aa
3 changed files with 65 additions and 30 deletions

View File

@@ -256,25 +256,27 @@ definitions:
example: 150
enrollment_counts:
type: object
description: Enrollment count breakdown by mode
description: |
Enrollment count breakdown by mode. Keys are the mode slugs
configured for the course (e.g. audit, verified, honor,
professional) plus a 'total' key. Only modes configured for
the course are included; unconfigured modes are omitted.
Modes with zero enrollments are included with a count of 0.
properties:
total:
type: integer
minimum: 0
audit:
type: integer
minimum: 0
verified:
type: integer
minimum: 0
honor:
type: integer
minimum: 0
description: Total enrollments across all modes
additionalProperties:
type: integer
minimum: 0
description: Enrollment count for a configured course mode
example:
total: 150
audit: 100
verified: 40
honor: 10
professional: 0
num_sections:
type: integer
minimum: 0

View File

@@ -24,6 +24,7 @@ from common.djangoapps.student.tests.factories import (
StaffFactory,
UserFactory,
)
from common.djangoapps.course_modes.tests.factories import CourseModeFactory
from common.djangoapps.student.models.course_enrollment import CourseEnrollment
from lms.djangoapps.courseware.models import StudentModule
from lms.djangoapps.instructor_task.tests.factories import InstructorTaskFactory
@@ -218,18 +219,59 @@ class CourseMetadataViewTest(SharedModuleStoreTestCase):
def test_enrollment_counts_by_mode(self):
"""
Test that enrollment counts include breakdown by mode.
Test that enrollment counts include all configured modes,
even those with zero enrollments.
"""
# Configure modes for the course: audit, verified, honor, and professional
for mode_slug in ('audit', 'verified', 'honor', 'professional'):
CourseModeFactory.create(course_id=self.course_key, mode_slug=mode_slug)
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self._get_url())
self.assertEqual(response.status_code, status.HTTP_200_OK)
enrollment_counts = response.data['enrollment_counts']
# Should have total count
# All configured modes should be present
self.assertIn('audit', enrollment_counts)
self.assertIn('verified', enrollment_counts)
self.assertIn('honor', enrollment_counts)
self.assertIn('professional', enrollment_counts)
self.assertIn('total', enrollment_counts)
# professional has no enrollments but should still appear with 0
self.assertEqual(enrollment_counts['professional'], 0)
# Modes with enrollments should have correct counts
self.assertGreaterEqual(enrollment_counts['audit'], 1)
self.assertGreaterEqual(enrollment_counts['verified'], 1)
self.assertGreaterEqual(enrollment_counts['honor'], 1)
self.assertGreaterEqual(enrollment_counts['total'], 3)
def test_enrollment_counts_excludes_unconfigured_modes(self):
"""
Test that enrollment counts only include modes configured for the course,
not modes that exist on other courses.
"""
# Only configure audit and honor for this course (not verified)
CourseModeFactory.create(course_id=self.course_key, mode_slug='audit')
CourseModeFactory.create(course_id=self.course_key, mode_slug='honor')
self.client.force_authenticate(user=self.instructor)
response = self.client.get(self._get_url())
self.assertEqual(response.status_code, status.HTTP_200_OK)
enrollment_counts = response.data['enrollment_counts']
# Only configured modes should appear
self.assertIn('audit', enrollment_counts)
self.assertIn('honor', enrollment_counts)
self.assertIn('total', enrollment_counts)
# verified is not configured, so it should not appear
# (even though there are verified enrollments from setUp)
self.assertNotIn('verified', enrollment_counts)
def _get_tabs_from_response(self, user, course_id=None):
"""Helper to get tabs from API response."""
self.client.force_authenticate(user=user)

View File

@@ -6,12 +6,12 @@ Following REST best practices, serializers encapsulate most of the data processi
"""
from django.conf import settings
from django.db.models import Count
from django.utils.html import escape
from django.utils.translation import gettext as _
from edx_when.api import is_enabled_for_course
from rest_framework import serializers
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import (
CourseFinanceAdminRole,
@@ -266,25 +266,16 @@ class CourseInformationSerializerV2(serializers.Serializer):
def get_total_enrollment(self, data):
"""Get total enrollment count."""
total_enrollments = CourseEnrollment.objects.filter(
course_id=data['course'].id,
is_active=True
).count()
return total_enrollments
return self.get_enrollment_counts(data)['total']
def get_enrollment_counts(self, data):
"""Get enrollment counts by mode."""
course = data['course']
total_enrollments = self.get_total_enrollment(data)
enrollments_by_mode = CourseEnrollment.objects.filter(
course_id=course.id,
is_active=True
).values('mode').annotate(count=Count('mode'))
by_mode = {item['mode']: item['count'] for item in enrollments_by_mode}
by_mode['total'] = total_enrollments
return by_mode
"""Get enrollment counts for all configured course modes."""
course_id = data['course'].id
counts = CourseEnrollment.objects.enrollment_counts(course_id)
configured_modes = CourseMode.modes_for_course(course_id)
result = {mode.slug: counts[mode.slug] for mode in configured_modes}
result['total'] = counts['total']
return result
def get_num_sections(self, data):
"""Get number of sections in the course."""