[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:
@@ -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, )
|
||||||
data = [
|
course_run_key = str(course1.id)
|
||||||
ProgramFactory(
|
course2 = ModuleStoreCourseFactory.create(self_paced=True, )
|
||||||
|
program = ProgramFactory(
|
||||||
courses=[
|
courses=[
|
||||||
CourseFactory(course_runs=[
|
CourseFactory(course_runs=[
|
||||||
CourseRunFactory(key=course_run_key, type='honor'),
|
CourseRunFactory(key=course_run_key, type='honor'),
|
||||||
CourseRunFactory(),
|
CourseRunFactory(key=str(course2.id)),
|
||||||
]),
|
]),
|
||||||
]
|
]
|
||||||
),
|
)
|
||||||
|
data = [
|
||||||
|
program,
|
||||||
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user