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): ...@@ -219,3 +219,7 @@ class CourseEntitlement(TimeStampedModel):
Fulfills an entitlement by specifying a session. Fulfills an entitlement by specifying a session.
""" """
cls.objects.filter(id=entitlement.id).update(enrollment_course_run=enrollment) 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): ...@@ -27,6 +27,7 @@ class CourseEntitlementFactory(factory.django.DjangoModelFactory):
uuid = factory.LazyFunction(uuid4) uuid = factory.LazyFunction(uuid4)
course_uuid = factory.LazyFunction(uuid4) course_uuid = factory.LazyFunction(uuid4)
expired_at = None
mode = FuzzyChoice([CourseMode.VERIFIED, CourseMode.PROFESSIONAL]) mode = FuzzyChoice([CourseMode.VERIFIED, CourseMode.PROFESSIONAL])
user = factory.SubFactory(UserFactory) user = factory.SubFactory(UserFactory)
order_number = FuzzyText(prefix='TEXTX', chars=string.digits) order_number = FuzzyText(prefix='TEXTX', chars=string.digits)
......
...@@ -67,6 +67,11 @@ class TestProgramProgressMeter(TestCase): ...@@ -67,6 +67,11 @@ class TestProgramProgressMeter(TestCase):
for course_run_id in course_run_ids: for course_run_id in course_run_ids:
CourseEnrollmentFactory(user=self.user, course_id=course_run_id, mode=CourseMode.VERIFIED) 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): def _assert_progress(self, meter, *progresses):
"""Variadic helper used to verify progress calculations.""" """Variadic helper used to verify progress calculations."""
self.assertEqual(meter.progress(), list(progresses)) self.assertEqual(meter.progress(), list(progresses))
...@@ -93,8 +98,8 @@ class TestProgramProgressMeter(TestCase): ...@@ -93,8 +98,8 @@ class TestProgramProgressMeter(TestCase):
return result return result
def test_no_enrollments(self, mock_get_programs): def test_no_enrollments_or_entitlements(self, mock_get_programs):
"""Verify behavior when programs exist, but no relevant enrollments do.""" """Verify behavior when programs exist, but no relevant enrollments or entitlements do."""
data = [ProgramFactory()] data = [ProgramFactory()]
mock_get_programs.return_value = data mock_get_programs.return_value = data
...@@ -104,7 +109,7 @@ class TestProgramProgressMeter(TestCase): ...@@ -104,7 +109,7 @@ class TestProgramProgressMeter(TestCase):
self._assert_progress(meter) self._assert_progress(meter)
self.assertEqual(meter.completed_programs, []) 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.""" """Verify behavior when enrollments exist, but no matching programs do."""
mock_get_programs.return_value = [] mock_get_programs.return_value = []
...@@ -116,7 +121,16 @@ class TestProgramProgressMeter(TestCase): ...@@ -116,7 +121,16 @@ class TestProgramProgressMeter(TestCase):
self._assert_progress(meter) self._assert_progress(meter)
self.assertEqual(meter.completed_programs, []) 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 Verify that correct program is returned when the user is enrolled in a
course run appearing in one program. course run appearing in one program.
...@@ -146,6 +160,25 @@ class TestProgramProgressMeter(TestCase): ...@@ -146,6 +160,25 @@ class TestProgramProgressMeter(TestCase):
) )
self.assertEqual(meter.completed_programs, []) 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): def test_course_progress(self, mock_get_programs):
""" """
Verify that the progress meter can represent progress in terms of Verify that the progress meter can represent progress in terms of
...@@ -259,7 +292,7 @@ class TestProgramProgressMeter(TestCase): ...@@ -259,7 +292,7 @@ class TestProgramProgressMeter(TestCase):
self.assertEqual(meter.progress(count_only=True), expected) 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 Verify that correct programs are returned in the correct order when the
user is enrolled in course runs appearing in programs. user is enrolled in course runs appearing in programs.
...@@ -303,6 +336,28 @@ class TestProgramProgressMeter(TestCase): ...@@ -303,6 +336,28 @@ class TestProgramProgressMeter(TestCase):
) )
self.assertEqual(meter.completed_programs, []) 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): def test_shared_enrollment_engagement(self, mock_get_programs):
""" """
Verify that correct programs are returned when the user is enrolled in a Verify that correct programs are returned when the user is enrolled in a
...@@ -354,6 +409,34 @@ class TestProgramProgressMeter(TestCase): ...@@ -354,6 +409,34 @@ class TestProgramProgressMeter(TestCase):
) )
self.assertEqual(meter.completed_programs, []) 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) @mock.patch(UTILS_MODULE + '.ProgramProgressMeter.completed_course_runs', new_callable=mock.PropertyMock)
def test_simulate_progress(self, mock_completed_course_runs, mock_get_programs): def test_simulate_progress(self, mock_completed_course_runs, 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."""
......
...@@ -19,6 +19,7 @@ from pytz import utc ...@@ -19,6 +19,7 @@ from pytz import utc
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from course_modes.models import CourseMode from course_modes.models import CourseMode
from entitlements.models import CourseEntitlement
from lms.djangoapps.certificates import api as certificate_api from lms.djangoapps.certificates import api as certificate_api
from lms.djangoapps.commerce.utils import EcommerceService from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.access import has_access
...@@ -90,6 +91,9 @@ class ProgramProgressMeter(object): ...@@ -90,6 +91,9 @@ class ProgramProgressMeter(object):
# We can't use dict.keys() for this because the course run ids need to be ordered # 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.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() self.course_grade_factory = CourseGradeFactory()
if uuid: if uuid:
...@@ -100,9 +104,9 @@ class ProgramProgressMeter(object): ...@@ -100,9 +104,9 @@ class ProgramProgressMeter(object):
def invert_programs(self): def invert_programs(self):
"""Intersect programs and enrollments. """Intersect programs and enrollments.
Builds a dictionary of program dict lists keyed by course run ID. The Builds a dictionary of program dict lists keyed by course run ID and by course UUID.
resulting dictionary is suitable in applications where programs must be The resulting dictionary is suitable in applications where programs must be
filtered by the course runs they contain (e.g., the student dashboard). filtered by the course runs or courses they contain (e.g., the student dashboard).
Returns: Returns:
defaultdict, programs keyed by course run ID defaultdict, programs keyed by course run ID
...@@ -111,6 +115,12 @@ class ProgramProgressMeter(object): ...@@ -111,6 +115,12 @@ class ProgramProgressMeter(object):
for program in self.programs: for program in self.programs:
for course in program['courses']: 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']: for course_run in course['course_runs']:
course_run_id = course_run['key'] course_run_id = course_run['key']
if course_run_id in self.course_run_ids: if course_run_id in self.course_run_ids:
...@@ -145,6 +155,11 @@ class ProgramProgressMeter(object): ...@@ -145,6 +155,11 @@ class ProgramProgressMeter(object):
if program not in programs: if program not in programs:
programs.append(program) 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 return programs
def _is_course_in_progress(self, now, course): 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