Commit 853cd9a0 by McKenzie Welter

show programs in which user holds a course entitlement on programs listing page

parent 25eb3187
......@@ -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):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment