From 8023a09191ffb51006d5c9d57ba74b0fb1296e03 Mon Sep 17 00:00:00 2001 From: Matt Hughes Date: Thu, 13 May 2021 15:21:06 -0400 Subject: [PATCH] fix: deleted courses do not break program details page Sometimes learners have certificates in old course runs which've been deleted. When this happens loading the learner's program progress can result in 500 errors. This corrects those by choosing to count any non-existent course the learner has certificates for as counting towards program completion, effectively assuming that availability dates have passed for all such courses. Also fixes an error with a condition related to how we determine whether an enrolled course is considered "in progress". The previous version of the code had a bug that would result in a lot more courses being marked "in progress" for the purpose of program completion than actually were. JIRA:EDUCATOR-5787 --- .../djangoapps/programs/tests/test_utils.py | 28 ++++++++++++++++++- openedx/core/djangoapps/programs/utils.py | 10 +++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index b61fdfe5ea..c6f9125125 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -598,7 +598,7 @@ class TestProgramProgressMeter(ModuleStoreTestCase): ProgressFactory( uuid=program_uuid, completed=2, - in_progress=1, + not_started=1, ) ) assert list(meter.completed_programs_with_available_dates.keys()) == [program_uuid] @@ -619,6 +619,32 @@ class TestProgramProgressMeter(ModuleStoreTestCase): assert list(meter.completed_programs_with_available_dates.keys()) == [program_uuid] assert meter.completed_programs_with_available_dates[program_uuid].date() == today.date() + def test_old_course_runs(self, mock_get_programs): + """ + Test that old course runs may exist for a program which do not exist in LMS. + In that case, continue considering the course run to've been failed by the learner + """ + course_run = CourseRunFactory.create() + course = CourseFactory.create(course_runs=[course_run]) + program_data = ProgramFactory(courses=[course]) + + data = [program_data] + mock_get_programs.return_value = data + + course_run_key = str(course_run['key']) + self._create_enrollments(course_run_key) + self._create_certificates(course_run_key, mode=MODES.verified) + + meter = ProgramProgressMeter(self.site, self.user) + + self._assert_progress( + meter, + ProgressFactory( + uuid=program_data['uuid'], + not_started=1 + ) + ) + def test_nonverified_course_run_completion(self, mock_get_programs): """ Course runs aren't necessarily of type verified. Verify that a program can diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index 5b34d97330..62ac9fb314 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -206,7 +206,7 @@ class ProgramProgressMeter: ] if runs_with_required_mode: - not_failed_runs = [run for run in runs_with_required_mode if run not in self.failed_course_runs] + not_failed_runs = [run for run in runs_with_required_mode if run['key'] not in self.failed_course_runs] if not_failed_runs: return True @@ -417,7 +417,7 @@ class ProgramProgressMeter: Determine which course runs have been failed by the user. Returns: - list of dicts, each a course run ID + list of strings, each a course run ID """ return [run['course_run_id'] for run in self.course_runs_with_state['failed']] @@ -442,9 +442,13 @@ class ProgramProgressMeter: 'type': self._certificate_mode_translation(certificate['type']), } + try: + may_certify = CourseOverview.get_from_id(course_key).may_certify() + except CourseOverview.DoesNotExist: + may_certify = True if ( certificate_api.is_passing_status(certificate['status']) - and CourseOverview.get_from_id(course_key).may_certify() + and may_certify ): completed_runs.append(course_data) else: