[bug] Corrected issue where program dash showed incorrect completed

count

[MICROBA-1163]

This change will correct an issue in the Program Dashboard where a user
would see a course as completed, but not see their Certificate because
it was not available to them yet.
This commit is contained in:
Albert (AJ) St. Aubin
2021-04-15 21:36:47 -04:00
parent a21d0ff224
commit a1fe3d58dc
2 changed files with 109 additions and 27 deletions

View File

@@ -1,6 +1,5 @@
"""Tests covering Programs utilities.""" """Tests covering Programs utilities."""
import datetime import datetime
import json import json
import uuid import uuid
@@ -9,6 +8,7 @@ from copy import deepcopy
from unittest import mock from unittest import mock
import ddt import ddt
from edx_toggles.toggles import LegacyWaffleSwitch
from edx_toggles.toggles.testutils import override_waffle_switch from edx_toggles.toggles.testutils import override_waffle_switch
import httpretty import httpretty
from django.conf import settings from django.conf import settings
@@ -34,6 +34,8 @@ from openedx.core.djangoapps.catalog.tests.factories import (
SeatFactory, SeatFactory,
generate_course_run_key generate_course_run_key
) )
from openedx.core.djangoapps.certificates.config import waffle
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER from openedx.core.djangoapps.programs import ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER
from openedx.core.djangoapps.programs.tests.factories import ProgressFactory from openedx.core.djangoapps.programs.tests.factories import ProgressFactory
from openedx.core.djangoapps.programs.utils import ( from openedx.core.djangoapps.programs.utils import (
@@ -58,12 +60,14 @@ from xmodule.modulestore.tests.factories import CourseFactory as ModuleStoreCour
ECOMMERCE_URL_ROOT = 'https://ecommerce.example.com' ECOMMERCE_URL_ROOT = 'https://ecommerce.example.com'
UTILS_MODULE = 'openedx.core.djangoapps.programs.utils' UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
LOGGER_NAME = 'openedx.core.djangoapps.programs.utils' LOGGER_NAME = 'openedx.core.djangoapps.programs.utils'
AUTO_CERTIFICATE_GENERATION_SWITCH = LegacyWaffleSwitch(waffle.waffle(), waffle.AUTO_CERTIFICATE_GENERATION) # pylint: disable=toggle-missing-annotation
@ddt.ddt @ddt.ddt
@skip_unless_lms @skip_unless_lms
@override_waffle_switch(AUTO_CERTIFICATE_GENERATION_SWITCH, active=True)
@mock.patch(UTILS_MODULE + '.get_programs') @mock.patch(UTILS_MODULE + '.get_programs')
class TestProgramProgressMeter(TestCase): class TestProgramProgressMeter(ModuleStoreTestCase):
"""Tests of the program progress utility class.""" """Tests of the program progress utility class."""
def setUp(self): def setUp(self):
@@ -473,7 +477,32 @@ class TestProgramProgressMeter(TestCase):
def test_simulate_progress(self, mock_get_programs): def test_simulate_progress(self, mock_get_programs):
"""Simulate the entirety of a user's progress through a program.""" """Simulate the entirety of a user's progress through a program."""
first_course_run_key, second_course_run_key = (generate_course_run_key() for __ in range(2)) today = datetime.datetime.now(utc)
two_days_ago = today - datetime.timedelta(days=2)
three_days_ago = today - datetime.timedelta(days=3)
yesterday = today - datetime.timedelta(days=1)
tomorrow = today + datetime.timedelta(days=1)
course1 = ModuleStoreCourseFactory.create(
start=yesterday,
end=tomorrow,
self_paced=True,
)
first_course_run_key = str(course1.id)
course2 = ModuleStoreCourseFactory.create(
start=yesterday,
end=tomorrow,
self_paced=True,
)
second_course_run_key = str(course2.id)
course3 = ModuleStoreCourseFactory.create(
start=three_days_ago,
end=two_days_ago,
self_paced=False,
certificate_available_date=tomorrow,
certificates_display_behavior='end'
)
third_course_run_key = str(course3.id)
data = [ data = [
ProgramFactory( ProgramFactory(
courses=[ courses=[
@@ -483,6 +512,9 @@ class TestProgramProgressMeter(TestCase):
CourseFactory(course_runs=[ CourseFactory(course_runs=[
CourseRunFactory(key=second_course_run_key), CourseRunFactory(key=second_course_run_key),
]), ]),
CourseFactory(course_runs=[
CourseRunFactory(key=third_course_run_key),
]),
] ]
), ),
ProgramFactory(), ProgramFactory(),
@@ -500,18 +532,19 @@ class TestProgramProgressMeter(TestCase):
_, program_uuid = data[0], data[0]['uuid'] _, program_uuid = data[0], data[0]['uuid']
self._assert_progress( self._assert_progress(
meter, meter,
ProgressFactory(uuid=program_uuid, in_progress=1, not_started=1) ProgressFactory(uuid=program_uuid, in_progress=1, not_started=2)
) )
assert list(meter.completed_programs_with_available_dates.keys()) == [] assert list(meter.completed_programs_with_available_dates.keys()) == []
# Two enrollments, all courses in progress. # 3 enrollments, 3 courses in progress.
self._create_enrollments(second_course_run_key) self._create_enrollments(second_course_run_key)
self._create_enrollments(third_course_run_key)
meter = ProgramProgressMeter(self.site, self.user) meter = ProgramProgressMeter(self.site, self.user)
self._assert_progress( self._assert_progress(
meter, meter,
ProgressFactory( ProgressFactory(
uuid=program_uuid, uuid=program_uuid,
in_progress=2, in_progress=3,
) )
) )
assert list(meter.completed_programs_with_available_dates.keys()) == [] assert list(meter.completed_programs_with_available_dates.keys()) == []
@@ -524,7 +557,7 @@ class TestProgramProgressMeter(TestCase):
ProgressFactory( ProgressFactory(
uuid=program_uuid, uuid=program_uuid,
completed=1, completed=1,
in_progress=1, in_progress=2,
) )
) )
assert list(meter.completed_programs_with_available_dates.keys()) == [] assert list(meter.completed_programs_with_available_dates.keys()) == []
@@ -538,12 +571,12 @@ class TestProgramProgressMeter(TestCase):
ProgressFactory( ProgressFactory(
uuid=program_uuid, uuid=program_uuid,
completed=1, completed=1,
in_progress=1, in_progress=2,
) )
) )
assert list(meter.completed_programs_with_available_dates.keys()) == [] assert list(meter.completed_programs_with_available_dates.keys()) == []
# Second valid certificate obtained, all courses complete. # Second valid certificate obtained, 2 courses complete.
second_cert.mode = MODES.verified second_cert.mode = MODES.verified
second_cert.save() second_cert.save()
meter = ProgramProgressMeter(self.site, self.user) meter = ProgramProgressMeter(self.site, self.user)
@@ -552,25 +585,58 @@ class TestProgramProgressMeter(TestCase):
ProgressFactory( ProgressFactory(
uuid=program_uuid, uuid=program_uuid,
completed=2, completed=2,
in_progress=1,
)
)
assert list(meter.completed_programs_with_available_dates.keys()) == []
# 3 certs, 1 unavailable, Program available in the future
self._create_certificates(third_course_run_key, mode=MODES.verified)
meter = ProgramProgressMeter(self.site, self.user)
self._assert_progress(
meter,
ProgressFactory(
uuid=program_uuid,
completed=2,
in_progress=1,
) )
) )
assert list(meter.completed_programs_with_available_dates.keys()) == [program_uuid] assert list(meter.completed_programs_with_available_dates.keys()) == [program_uuid]
assert meter.completed_programs_with_available_dates[program_uuid] > today
# 3 certs, all available, program cert in the past/now
course3_overview = CourseOverview.get_from_id(course3.id)
course3_overview.certificate_available_date = yesterday
course3_overview.save()
meter = ProgramProgressMeter(self.site, self.user)
self._assert_progress(
meter,
ProgressFactory(
uuid=program_uuid,
completed=3,
)
)
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_nonverified_course_run_completion(self, mock_get_programs): def test_nonverified_course_run_completion(self, mock_get_programs):
""" """
Course runs aren't necessarily of type verified. Verify that a program can Course runs aren't necessarily of type verified. Verify that a program can
still be completed when this is the case. still be completed when this is the case.
""" """
course_run_key = generate_course_run_key() course1 = ModuleStoreCourseFactory.create(self_paced=True, )
course_run_key = str(course1.id)
course2 = ModuleStoreCourseFactory.create(self_paced=True, )
program = ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=course_run_key, type='honor'),
CourseRunFactory(key=str(course2.id)),
]),
]
)
data = [ data = [
ProgramFactory( program,
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=course_run_key, type='honor'),
CourseRunFactory(),
]),
]
),
ProgramFactory(), ProgramFactory(),
] ]
mock_get_programs.return_value = data mock_get_programs.return_value = data
@@ -579,7 +645,7 @@ class TestProgramProgressMeter(TestCase):
self._create_certificates(course_run_key) self._create_certificates(course_run_key)
meter = ProgramProgressMeter(self.site, self.user) meter = ProgramProgressMeter(self.site, self.user)
_, program_uuid = data[0], data[0]['uuid'] program_uuid = program['uuid']
self._assert_progress( self._assert_progress(
meter, meter,
ProgressFactory(uuid=program_uuid, completed=1) ProgressFactory(uuid=program_uuid, completed=1)
@@ -623,6 +689,7 @@ class TestProgramProgressMeter(TestCase):
if str(cert.course_id) == run_course2['key']: if str(cert.course_id) == run_course2['key']:
return datetime.datetime(2016, 1, 1) return datetime.datetime(2016, 1, 1)
return datetime.datetime(2015, 1, 1) return datetime.datetime(2015, 1, 1)
mock_available_date_for_certificate.side_effect = available_date_fake mock_available_date_for_certificate.side_effect = available_date_fake
meter = ProgramProgressMeter(self.site, self.user) meter = ProgramProgressMeter(self.site, self.user)
@@ -635,14 +702,21 @@ class TestProgramProgressMeter(TestCase):
""" """
Verify that the method can find course run certificates when not mocked out. Verify that the method can find course run certificates when not mocked out.
""" """
downloadable = CourseRunFactory() downloadable_module_store_course = ModuleStoreCourseFactory.create(self_paced=True, )
generating = CourseRunFactory() downloadable = CourseRunFactory(key=downloadable_module_store_course.id)
course_availability_in_future = CourseRunFactory()
generating_module_store_course = ModuleStoreCourseFactory.create(self_paced=True, )
generating = CourseRunFactory(key=generating_module_store_course.id)
unknown = CourseRunFactory() unknown = CourseRunFactory()
course = CourseFactory(course_runs=[downloadable, generating, unknown]) course = CourseFactory(course_runs=[downloadable, course_availability_in_future, generating, unknown])
program = ProgramFactory(courses=[course]) program = ProgramFactory(courses=[course])
mock_get_programs.return_value = [program] mock_get_programs.return_value = [program]
self._create_enrollments(downloadable['key'], generating['key'], unknown['key']) self._create_enrollments(
downloadable['key'],
generating['key'],
unknown['key']
)
self._create_certificates(downloadable['key'], mode=CourseMode.VERIFIED) self._create_certificates(downloadable['key'], mode=CourseMode.VERIFIED)
self._create_certificates(generating['key'], status='generating', mode=CourseMode.HONOR) self._create_certificates(generating['key'], status='generating', mode=CourseMode.HONOR)
@@ -652,8 +726,8 @@ class TestProgramProgressMeter(TestCase):
self.assertCountEqual( self.assertCountEqual(
meter.completed_course_runs, meter.completed_course_runs,
[ [
{'course_run_id': downloadable['key'], 'type': CourseMode.VERIFIED}, {'course_run_id': str(downloadable['key']), 'type': CourseMode.VERIFIED},
{'course_run_id': generating['key'], 'type': CourseMode.HONOR}, {'course_run_id': str(generating['key']), 'type': CourseMode.HONOR},
] ]
) )
@@ -1243,6 +1317,7 @@ class TestGetCertificates(TestCase):
""" """
Tests of the function used to get certificates associated with a program. Tests of the function used to get certificates associated with a program.
""" """
def setUp(self): def setUp(self):
super().setUp() super().setUp()

View File

@@ -426,6 +426,9 @@ class ProgramProgressMeter:
""" """
Determine which course runs have been completed and failed by the user. Determine which course runs have been completed and failed by the user.
A course run is considered completed for a user if they have a certificate in the correct state and
the certificate is available.
Returns: Returns:
dict with a list of completed and failed runs dict with a list of completed and failed runs
""" """
@@ -433,12 +436,16 @@ class ProgramProgressMeter:
completed_runs, failed_runs = [], [] completed_runs, failed_runs = [], []
for certificate in course_run_certificates: for certificate in course_run_certificates:
course_key = certificate['course_key']
course_data = { course_data = {
'course_run_id': str(certificate['course_key']), 'course_run_id': str(course_key),
'type': self._certificate_mode_translation(certificate['type']), 'type': self._certificate_mode_translation(certificate['type']),
} }
if certificate_api.is_passing_status(certificate['status']): if (
certificate_api.is_passing_status(certificate['status'])
and CourseOverview.get_from_id(course_key).may_certify()
):
completed_runs.append(course_data) completed_runs.append(course_data)
else: else:
failed_runs.append(course_data) failed_runs.append(course_data)