fix: optimize enrollment counts to use read replica and show all configured modes (#38006)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user