Skip to content

Commit 46c9b3a

Browse files
authored
fix: add error handling for potential issues calculating course progress (#36660)
* fix: add error handling for potential issues calculating course progress Realized there is potential in a few places for exceptions to be thrown. I'm adding some more error handling in the course progress task and function to handle potential issues.
1 parent 42a9fb8 commit 46c9b3a

4 files changed

Lines changed: 66 additions & 8 deletions

File tree

lms/djangoapps/course_home_api/progress/api.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
from lms.djangoapps.courseware.courses import get_course_blocks_completion_summary
99

10-
1110
User = get_user_model()
1211

1312

@@ -28,9 +27,12 @@ def calculate_progress_for_learner_in_course(course_key: CourseKey, user: User)
2827
# refactored in the future so that the calculation is handled solely on the backend, eliminating the need for it to
2928
# be done in the frontend.
3029
num_total_units = complete_count + incomplete_count + locked_count
31-
complete_percentage = round(complete_count / num_total_units, 2)
32-
locked_percentage = round(locked_count / num_total_units, 2)
33-
incomplete_percentage = 1.00 - complete_percentage - locked_percentage
30+
if num_total_units == 0:
31+
complete_percentage = locked_percentage = incomplete_percentage = 0.0
32+
else:
33+
complete_percentage = round(complete_count / num_total_units, 2)
34+
locked_percentage = round(locked_count / num_total_units, 2)
35+
incomplete_percentage = 1.00 - complete_percentage - locked_percentage
3436

3537
return {
3638
"complete_count": complete_count,

lms/djangoapps/course_home_api/progress/tests/test_api.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ class ProgressApiTests(TestCase):
1313
"""
1414
Tests for the progress calculation functions.
1515
"""
16+
1617
@patch("lms.djangoapps.course_home_api.progress.api.get_course_blocks_completion_summary")
1718
def test_calculate_progress_for_learner_in_course(self, mock_get_summary):
1819
"""
1920
A test to verify functionality of the function under test.
2021
"""
21-
get_summary_return_val = {
22+
mock_get_summary.return_value = {
2223
"complete_count": 5,
2324
"incomplete_count": 2,
2425
"locked_count": 1,
2526
}
26-
mock_get_summary.return_value = get_summary_return_val
2727

2828
expected_data = {
2929
"complete_count": 5,
@@ -39,6 +39,31 @@ def test_calculate_progress_for_learner_in_course(self, mock_get_summary):
3939
assert mock_get_summary.called_once_with("some_course", "some_user")
4040
assert results == expected_data
4141

42+
@patch("lms.djangoapps.course_home_api.progress.api.get_course_blocks_completion_summary")
43+
def test_handle_division_by_zero(self, mock_get_summary):
44+
"""
45+
A test to verify that we're avoiding division-by-zero errors if the total number of units is 0.
46+
"""
47+
mock_get_summary.return_value = {
48+
"complete_count": 0,
49+
"incomplete_count": 0,
50+
"locked_count": 0,
51+
}
52+
53+
expected_data = {
54+
"complete_count": 0,
55+
"incomplete_count": 0,
56+
"locked_count": 0,
57+
"total_count": 0,
58+
"complete_percentage": 0.0,
59+
"locked_percentage": 0.0,
60+
"incomplete_percentage": 0.0,
61+
}
62+
63+
results = calculate_progress_for_learner_in_course("some_course", "some_user")
64+
assert mock_get_summary.called_once_with("some_course", "some_user")
65+
assert results == expected_data
66+
4267
@patch("lms.djangoapps.course_home_api.progress.api.get_course_blocks_completion_summary")
4368
def test_calculate_progress_for_learner_in_course_summary_empty(self, mock_get_summary):
4469
"""

lms/djangoapps/course_home_api/tasks.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,19 @@ def collect_progress_for_user_in_course(course_id: str, user_id: str) -> None:
3737
log.warning(f"Could not retrieve a user with id {user_id}, aborting task.")
3838
return
3939

40+
try:
41+
enrollment = get_course_enrollment(user, course_key)
42+
enrollment_mode = enrollment.mode
43+
except AttributeError:
44+
log.warning(f"Could not retrieve enrollment info for user {user.id} in course {course_id}")
45+
return
46+
4047
progress = calculate_progress_for_learner_in_course(course_key, user)
41-
enrollment = get_course_enrollment(user, course_key)
48+
4249
# add a few extra fields to the returned data to make the event payload a bit more usable
4350
progress["user_id"] = user.id
4451
progress["course_id"] = course_id
45-
progress["enrollment_mode"] = enrollment.mode
52+
progress["enrollment_mode"] = enrollment_mode
4653

4754
tracker.emit(
4855
COURSE_COMPLETION_FOR_USER_EVENT_NAME,

lms/djangoapps/course_home_api/tests/test_tasks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from unittest.mock import patch
66

77
from opaque_keys.edx.keys import CourseKey
8+
from testfixtures import LogCapture
89
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
910

1011
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
@@ -14,6 +15,8 @@
1415
)
1516
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory, CourseRunFactory
1617

18+
LOG_PATH = 'lms.djangoapps.course_home_api.tasks'
19+
1720

1821
class CalculateCompletionTaskTests(ModuleStoreTestCase):
1922
"""
@@ -67,6 +70,27 @@ def test_successful_event_emission(self, mock_tracker, mock_progress):
6770
expected_data,
6871
)
6972

73+
@patch("lms.djangoapps.course_home_api.tasks.calculate_progress_for_learner_in_course")
74+
@patch("lms.djangoapps.course_home_api.tasks.get_course_enrollment")
75+
@patch("lms.djangoapps.course_home_api.tasks.tracker.emit")
76+
def test_cannot_retrieve_enrollment_info(self, mock_tracker, mock_get_enrollment, mock_progress):
77+
"""
78+
Test to ensure the task is aborted if we cannot retrieve enrollment info for the user in the specified course.
79+
"""
80+
mock_get_enrollment.return_value = None
81+
82+
expected_message = (
83+
f"Could not retrieve enrollment info for user {self.user.id} in course {self.course_run_key_string}"
84+
)
85+
86+
with LogCapture() as log:
87+
collect_progress_for_user_in_course(self.course_run_key_string, self.user.id)
88+
89+
mock_get_enrollment.assert_called_once_with(self.user, CourseKey.from_string(self.course_run_key_string))
90+
log.check_present((LOG_PATH, "WARNING", expected_message),)
91+
mock_progress.assert_not_called()
92+
mock_tracker.assert_not_called()
93+
7094
@patch("lms.djangoapps.course_home_api.tasks.calculate_progress_for_learner_in_course")
7195
@patch("lms.djangoapps.course_home_api.tasks.tracker.emit")
7296
def test_aborted_task_user_dne(self, mock_tracker, mock_progress):

0 commit comments

Comments
 (0)