[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."""
import datetime
import json
import uuid
@@ -9,6 +8,7 @@ from copy import deepcopy
from unittest import mock
import ddt
from edx_toggles.toggles import LegacyWaffleSwitch
from edx_toggles.toggles.testutils import override_waffle_switch
import httpretty
from django.conf import settings
@@ -34,6 +34,8 @@ from openedx.core.djangoapps.catalog.tests.factories import (
SeatFactory,
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.tests.factories import ProgressFactory
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'
UTILS_MODULE = '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
@skip_unless_lms
@override_waffle_switch(AUTO_CERTIFICATE_GENERATION_SWITCH, active=True)
@mock.patch(UTILS_MODULE + '.get_programs')
class TestProgramProgressMeter(TestCase):
class TestProgramProgressMeter(ModuleStoreTestCase):
"""Tests of the program progress utility class."""
def setUp(self):
@@ -473,7 +477,32 @@ class TestProgramProgressMeter(TestCase):
def test_simulate_progress(self, mock_get_programs):
"""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 = [
ProgramFactory(
courses=[
@@ -483,6 +512,9 @@ class TestProgramProgressMeter(TestCase):
CourseFactory(course_runs=[
CourseRunFactory(key=second_course_run_key),
]),
CourseFactory(course_runs=[
CourseRunFactory(key=third_course_run_key),
]),
]
),
ProgramFactory(),
@@ -500,18 +532,19 @@ class TestProgramProgressMeter(TestCase):
_, program_uuid = data[0], data[0]['uuid']
self._assert_progress(
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()) == []
# Two enrollments, all courses in progress.
# 3 enrollments, 3 courses in progress.
self._create_enrollments(second_course_run_key)
self._create_enrollments(third_course_run_key)
meter = ProgramProgressMeter(self.site, self.user)
self._assert_progress(
meter,
ProgressFactory(
uuid=program_uuid,
in_progress=2,
in_progress=3,
)
)
assert list(meter.completed_programs_with_available_dates.keys()) == []
@@ -524,7 +557,7 @@ class TestProgramProgressMeter(TestCase):
ProgressFactory(
uuid=program_uuid,
completed=1,
in_progress=1,
in_progress=2,
)
)
assert list(meter.completed_programs_with_available_dates.keys()) == []
@@ -538,12 +571,12 @@ class TestProgramProgressMeter(TestCase):
ProgressFactory(
uuid=program_uuid,
completed=1,
in_progress=1,
in_progress=2,
)
)
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.save()
meter = ProgramProgressMeter(self.site, self.user)
@@ -552,25 +585,58 @@ class TestProgramProgressMeter(TestCase):
ProgressFactory(
uuid=program_uuid,
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 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):
"""
Course runs aren't necessarily of type verified. Verify that a program can
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 = [
ProgramFactory(
courses=[
CourseFactory(course_runs=[
CourseRunFactory(key=course_run_key, type='honor'),
CourseRunFactory(),
]),
]
),
program,
ProgramFactory(),
]
mock_get_programs.return_value = data
@@ -579,7 +645,7 @@ class TestProgramProgressMeter(TestCase):
self._create_certificates(course_run_key)
meter = ProgramProgressMeter(self.site, self.user)
_, program_uuid = data[0], data[0]['uuid']
program_uuid = program['uuid']
self._assert_progress(
meter,
ProgressFactory(uuid=program_uuid, completed=1)
@@ -623,6 +689,7 @@ class TestProgramProgressMeter(TestCase):
if str(cert.course_id) == run_course2['key']:
return datetime.datetime(2016, 1, 1)
return datetime.datetime(2015, 1, 1)
mock_available_date_for_certificate.side_effect = available_date_fake
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.
"""
downloadable = CourseRunFactory()
generating = CourseRunFactory()
downloadable_module_store_course = ModuleStoreCourseFactory.create(self_paced=True, )
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()
course = CourseFactory(course_runs=[downloadable, generating, unknown])
course = CourseFactory(course_runs=[downloadable, course_availability_in_future, generating, unknown])
program = ProgramFactory(courses=[course])
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(generating['key'], status='generating', mode=CourseMode.HONOR)
@@ -652,8 +726,8 @@ class TestProgramProgressMeter(TestCase):
self.assertCountEqual(
meter.completed_course_runs,
[
{'course_run_id': downloadable['key'], 'type': CourseMode.VERIFIED},
{'course_run_id': generating['key'], 'type': CourseMode.HONOR},
{'course_run_id': str(downloadable['key']), 'type': CourseMode.VERIFIED},
{'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.
"""
def setUp(self):
super().setUp()

View File

@@ -426,6 +426,9 @@ class ProgramProgressMeter:
"""
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:
dict with a list of completed and failed runs
"""
@@ -433,12 +436,16 @@ class ProgramProgressMeter:
completed_runs, failed_runs = [], []
for certificate in course_run_certificates:
course_key = certificate['course_key']
course_data = {
'course_run_id': str(certificate['course_key']),
'course_run_id': str(course_key),
'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)
else:
failed_runs.append(course_data)