From c8af55db6f4ea6a9e4812290ebf6e54c79a7a3c3 Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Tue, 3 Jan 2023 14:57:20 -0500 Subject: [PATCH] fix: handle exception getting cert statuses (#31490) If a user has a certificate in a deleted course, an issue with how the course is loaded from the cache can cause an exception that breaks our code. This adds a wrapper to fail gracefully and log the exception for future tracking / investigation. --- lms/djangoapps/learner_home/test_views.py | 36 +++++++++++++++++++++++ lms/djangoapps/learner_home/views.py | 22 +++++++++++--- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/learner_home/test_views.py b/lms/djangoapps/learner_home/test_views.py index 78e69a0224..79d127ab12 100644 --- a/lms/djangoapps/learner_home/test_views.py +++ b/lms/djangoapps/learner_home/test_views.py @@ -649,6 +649,42 @@ class TestDashboardView(BaseTestDashboardView): }, ) + @patch.dict(settings.FEATURES, ENTERPRISE_ENABLED=False) + @patch("lms.djangoapps.learner_home.views.cert_info") + def test_get_cert_statuses_exception(self, mock_get_cert_info): + """Test that cert information gets loaded correctly""" + + # Given I am logged in + self.log_in() + + # (and we have tons of mocks to avoid integration tests) + mock_enrollment = create_test_enrollment( + self.user, course_mode=CourseMode.VERIFIED + ) + + # but have an issue with a particular certificate + mock_get_cert_info.side_effect = Exception("test exception") + + # When I request the dashboard + response = self.client.get(self.view_url) + + # Then I get the expected success response + assert response.status_code == 200 + response_data = json.loads(response.content) + + empty_cert_data = { + "availableDate": None, + "isRestricted": False, + "isEarned": False, + "isDownloadable": False, + "certPreviewUrl": None, + } + + # with empty cert data instead of a break + self.assertDictEqual( + response_data["courses"][0]["certificate"], empty_cert_data + ) + @patch.dict(settings.FEATURES, ENTERPRISE_ENABLED=False) @patch("openedx.core.djangoapps.programs.utils.get_programs") def test_get_for_one_of_course_programs(self, mock_get_programs): diff --git a/lms/djangoapps/learner_home/views.py b/lms/djangoapps/learner_home/views.py index a4b75e93f0..db7a3cfa4c 100644 --- a/lms/djangoapps/learner_home/views.py +++ b/lms/djangoapps/learner_home/views.py @@ -238,10 +238,24 @@ def get_ecommerce_payment_page(user): @function_trace("get_cert_statuses") def get_cert_statuses(user, course_enrollments): """Get cert status by course for user enrollments""" - return { - enrollment.course_id: cert_info(user, enrollment) - for enrollment in course_enrollments - } + + cert_statuses = {} + + for enrollment in course_enrollments: + # APER-2171 - trying to get a cert for a deleted course can throw an exception + # Wrap in exception handling to avoid this issue. + try: + certificate_for_course = cert_info(user, enrollment) + + if certificate_for_course: + cert_statuses[enrollment.course_id] = certificate_for_course + + except Exception as ex: # pylint: disable=broad-except + logger.exception( + f"Error getting certificate status for (user, course) ({user}, {enrollment.course_id}): {ex}" + ) + + return cert_statuses @function_trace("get_org_block_and_allow_lists")