show programs in which user holds a course entitlement on programs listing page
This commit is contained in:
@@ -219,3 +219,7 @@ class CourseEntitlement(TimeStampedModel):
|
||||
Fulfills an entitlement by specifying a session.
|
||||
"""
|
||||
cls.objects.filter(id=entitlement.id).update(enrollment_course_run=enrollment)
|
||||
|
||||
@classmethod
|
||||
def unexpired_entitlements_for_user(cls, user):
|
||||
return cls.objects.filter(user=user, expired_at=None).select_related('user')
|
||||
|
||||
@@ -27,6 +27,7 @@ class CourseEntitlementFactory(factory.django.DjangoModelFactory):
|
||||
|
||||
uuid = factory.LazyFunction(uuid4)
|
||||
course_uuid = factory.LazyFunction(uuid4)
|
||||
expired_at = None
|
||||
mode = FuzzyChoice([CourseMode.VERIFIED, CourseMode.PROFESSIONAL])
|
||||
user = factory.SubFactory(UserFactory)
|
||||
order_number = FuzzyText(prefix='TEXTX', chars=string.digits)
|
||||
|
||||
@@ -67,6 +67,11 @@ class TestProgramProgressMeter(TestCase):
|
||||
for course_run_id in course_run_ids:
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_id, mode=CourseMode.VERIFIED)
|
||||
|
||||
def _create_entitlements(self, *course_uuids):
|
||||
""" Variadic helper used to create course entitlements. """
|
||||
for course_uuid in course_uuids:
|
||||
CourseEntitlementFactory(user=self.user, course_uuid=course_uuid)
|
||||
|
||||
def _assert_progress(self, meter, *progresses):
|
||||
"""Variadic helper used to verify progress calculations."""
|
||||
self.assertEqual(meter.progress(), list(progresses))
|
||||
@@ -93,8 +98,8 @@ class TestProgramProgressMeter(TestCase):
|
||||
|
||||
return result
|
||||
|
||||
def test_no_enrollments(self, mock_get_programs):
|
||||
"""Verify behavior when programs exist, but no relevant enrollments do."""
|
||||
def test_no_enrollments_or_entitlements(self, mock_get_programs):
|
||||
"""Verify behavior when programs exist, but no relevant enrollments or entitlements do."""
|
||||
data = [ProgramFactory()]
|
||||
mock_get_programs.return_value = data
|
||||
|
||||
@@ -104,7 +109,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
self._assert_progress(meter)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
def test_no_programs(self, mock_get_programs):
|
||||
def test_enrollments_but_no_programs(self, mock_get_programs):
|
||||
"""Verify behavior when enrollments exist, but no matching programs do."""
|
||||
mock_get_programs.return_value = []
|
||||
|
||||
@@ -116,7 +121,16 @@ class TestProgramProgressMeter(TestCase):
|
||||
self._assert_progress(meter)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
def test_single_program_engagement(self, mock_get_programs):
|
||||
def test_entitlements_but_no_programs(self, mock_get_programs):
|
||||
""" Verify engaged_programs is empty when entitlements exist, but no matching programs do. """
|
||||
mock_get_programs.return_value = []
|
||||
|
||||
self._create_entitlements(uuid.uuid4())
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
|
||||
self.assertEqual(meter.engaged_programs, [])
|
||||
|
||||
def test_single_program_enrollment(self, mock_get_programs):
|
||||
"""
|
||||
Verify that correct program is returned when the user is enrolled in a
|
||||
course run appearing in one program.
|
||||
@@ -146,6 +160,25 @@ class TestProgramProgressMeter(TestCase):
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
def test_single_program_entitlement(self, mock_get_programs):
|
||||
"""
|
||||
Verify that the correct program is returned when the user holds an entitlement
|
||||
to a course appearing in one program.
|
||||
"""
|
||||
course_uuid = uuid.uuid4()
|
||||
data = [
|
||||
ProgramFactory(courses=[CourseFactory(uuid=str(course_uuid))]),
|
||||
ProgramFactory(),
|
||||
]
|
||||
mock_get_programs.return_value = data
|
||||
|
||||
self._create_entitlements(course_uuid)
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
|
||||
self._attach_detail_url(data)
|
||||
program = data[0]
|
||||
self.assertEqual(meter.engaged_programs, [program])
|
||||
|
||||
def test_course_progress(self, mock_get_programs):
|
||||
"""
|
||||
Verify that the progress meter can represent progress in terms of
|
||||
@@ -259,7 +292,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
|
||||
self.assertEqual(meter.progress(count_only=True), expected)
|
||||
|
||||
def test_mutiple_program_engagement(self, mock_get_programs):
|
||||
def test_mutiple_program_enrollment(self, mock_get_programs):
|
||||
"""
|
||||
Verify that correct programs are returned in the correct order when the
|
||||
user is enrolled in course runs appearing in programs.
|
||||
@@ -303,6 +336,28 @@ class TestProgramProgressMeter(TestCase):
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
def test_multiple_program_entitlement(self, mock_get_programs):
|
||||
"""
|
||||
Verify that the correct programs are returned in the correct order
|
||||
when the user holds entitlements to courses appearing in those programs.
|
||||
"""
|
||||
newer_course_uuid, older_course_uuid = (uuid.uuid4() for __ in range(2))
|
||||
data = [
|
||||
ProgramFactory(courses=[CourseFactory(uuid=str(older_course_uuid)), ]),
|
||||
ProgramFactory(courses=[CourseFactory(uuid=str(newer_course_uuid)), ]),
|
||||
ProgramFactory(),
|
||||
]
|
||||
mock_get_programs.return_value = data
|
||||
|
||||
# The creation time of the entitlements matters to the test. We want
|
||||
# the newer_course_uuid to represent the newest entitlement.
|
||||
self._create_entitlements(older_course_uuid, newer_course_uuid)
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
|
||||
self._attach_detail_url(data)
|
||||
programs = data[:2]
|
||||
self.assertEqual(meter.engaged_programs, programs)
|
||||
|
||||
def test_shared_enrollment_engagement(self, mock_get_programs):
|
||||
"""
|
||||
Verify that correct programs are returned when the user is enrolled in a
|
||||
@@ -354,6 +409,34 @@ class TestProgramProgressMeter(TestCase):
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
def test_shared_entitlement_engagement(self, mock_get_programs):
|
||||
"""
|
||||
Verify that correct programs are returned when the user holds an entitlement
|
||||
to a single course appearing in multiple programs.
|
||||
"""
|
||||
shared_course_uuid, solo_course_uuid = (uuid.uuid4() for __ in range(2))
|
||||
|
||||
batch = [
|
||||
ProgramFactory(courses=[CourseFactory(uuid=str(shared_course_uuid)), ])
|
||||
for __ in range(2)
|
||||
]
|
||||
|
||||
joint_programs = sorted(batch, key=lambda program: program['title'])
|
||||
data = joint_programs + [
|
||||
ProgramFactory(courses=[CourseFactory(uuid=str(solo_course_uuid)), ]),
|
||||
ProgramFactory(),
|
||||
]
|
||||
|
||||
mock_get_programs.return_value = data
|
||||
|
||||
# Entitlement for the shared course created last (most recently).
|
||||
self._create_entitlements(shared_course_uuid, solo_course_uuid)
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
|
||||
self._attach_detail_url(data)
|
||||
programs = data[:3]
|
||||
self.assertEqual(meter.engaged_programs, programs)
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.ProgramProgressMeter.completed_course_runs', new_callable=mock.PropertyMock)
|
||||
def test_simulate_progress(self, mock_completed_course_runs, mock_get_programs):
|
||||
"""Simulate the entirety of a user's progress through a program."""
|
||||
|
||||
@@ -19,6 +19,7 @@ from pytz import utc
|
||||
from requests.exceptions import ConnectionError, Timeout
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from entitlements.models import CourseEntitlement
|
||||
from lms.djangoapps.certificates import api as certificate_api
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
from lms.djangoapps.courseware.access import has_access
|
||||
@@ -90,6 +91,9 @@ class ProgramProgressMeter(object):
|
||||
# We can't use dict.keys() for this because the course run ids need to be ordered
|
||||
self.course_run_ids.append(enrollment_id)
|
||||
|
||||
self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user))
|
||||
self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements]
|
||||
|
||||
self.course_grade_factory = CourseGradeFactory()
|
||||
|
||||
if uuid:
|
||||
@@ -100,9 +104,9 @@ class ProgramProgressMeter(object):
|
||||
def invert_programs(self):
|
||||
"""Intersect programs and enrollments.
|
||||
|
||||
Builds a dictionary of program dict lists keyed by course run ID. The
|
||||
resulting dictionary is suitable in applications where programs must be
|
||||
filtered by the course runs they contain (e.g., the student dashboard).
|
||||
Builds a dictionary of program dict lists keyed by course run ID and by course UUID.
|
||||
The resulting dictionary is suitable in applications where programs must be
|
||||
filtered by the course runs or courses they contain (e.g., the student dashboard).
|
||||
|
||||
Returns:
|
||||
defaultdict, programs keyed by course run ID
|
||||
@@ -111,6 +115,12 @@ class ProgramProgressMeter(object):
|
||||
|
||||
for program in self.programs:
|
||||
for course in program['courses']:
|
||||
course_uuid = course['uuid']
|
||||
if course_uuid in self.course_uuids:
|
||||
program_list = inverted_programs[course_uuid]
|
||||
if program not in program_list:
|
||||
program_list.append(program)
|
||||
continue
|
||||
for course_run in course['course_runs']:
|
||||
course_run_id = course_run['key']
|
||||
if course_run_id in self.course_run_ids:
|
||||
@@ -145,6 +155,11 @@ class ProgramProgressMeter(object):
|
||||
if program not in programs:
|
||||
programs.append(program)
|
||||
|
||||
for course_uuid in self.course_uuids:
|
||||
for program in inverted_programs[course_uuid]:
|
||||
if program not in programs:
|
||||
programs.append(program)
|
||||
|
||||
return programs
|
||||
|
||||
def _is_course_in_progress(self, now, course):
|
||||
|
||||
Reference in New Issue
Block a user