Consider user entitlements and use entitlement products in bundle one-click purchase
This commit is contained in:
committed by
McKenzie Welter
parent
c25e4ba045
commit
f72cf800e1
@@ -136,10 +136,10 @@ class CourseMode(models.Model):
|
||||
|
||||
HONOR = 'honor'
|
||||
PROFESSIONAL = 'professional'
|
||||
VERIFIED = "verified"
|
||||
AUDIT = "audit"
|
||||
NO_ID_PROFESSIONAL_MODE = "no-id-professional"
|
||||
CREDIT_MODE = "credit"
|
||||
VERIFIED = 'verified'
|
||||
AUDIT = 'audit'
|
||||
NO_ID_PROFESSIONAL_MODE = 'no-id-professional'
|
||||
CREDIT_MODE = 'credit'
|
||||
|
||||
DEFAULT_MODE = Mode(
|
||||
settings.COURSE_MODE_DEFAULTS['slug'],
|
||||
|
||||
@@ -24,3 +24,10 @@ class CourseEntitlement(TimeStampedModel):
|
||||
help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.'
|
||||
)
|
||||
order_number = models.CharField(max_length=128, null=True)
|
||||
|
||||
@property
|
||||
def expired_at_datetime(self):
|
||||
"""
|
||||
Getter to be used instead of expired_at because of the conditional check and update
|
||||
"""
|
||||
return self.expired_at
|
||||
|
||||
@@ -8,6 +8,7 @@ from faker import Faker
|
||||
|
||||
|
||||
fake = Faker()
|
||||
VERIFIED_MODE = 'verified'
|
||||
|
||||
|
||||
def generate_instances(factory_class, count=3):
|
||||
@@ -103,10 +104,18 @@ class SeatFactory(DictFactoryBase):
|
||||
currency = 'USD'
|
||||
price = factory.Faker('random_int')
|
||||
sku = factory.LazyFunction(generate_seat_sku)
|
||||
type = 'verified'
|
||||
type = VERIFIED_MODE
|
||||
upgrade_deadline = factory.LazyFunction(generate_zulu_datetime)
|
||||
|
||||
|
||||
class EntitlementFactory(DictFactoryBase):
|
||||
currency = 'USD'
|
||||
price = factory.Faker('random_int')
|
||||
sku = factory.LazyFunction(generate_seat_sku)
|
||||
mode = VERIFIED_MODE
|
||||
expires = None
|
||||
|
||||
|
||||
class CourseRunFactory(DictFactoryBase):
|
||||
eligible_for_financial_aid = True
|
||||
end = factory.LazyFunction(generate_zulu_datetime)
|
||||
@@ -121,7 +130,7 @@ class CourseRunFactory(DictFactoryBase):
|
||||
start = factory.LazyFunction(generate_zulu_datetime)
|
||||
status = 'published'
|
||||
title = factory.Faker('catch_phrase')
|
||||
type = 'verified'
|
||||
type = VERIFIED_MODE
|
||||
uuid = factory.Faker('uuid4')
|
||||
content_language = 'en'
|
||||
max_effort = 4
|
||||
@@ -130,6 +139,7 @@ class CourseRunFactory(DictFactoryBase):
|
||||
|
||||
class CourseFactory(DictFactoryBase):
|
||||
course_runs = factory.LazyFunction(partial(generate_instances, CourseRunFactory))
|
||||
entitlements = factory.LazyFunction(partial(generate_instances, EntitlementFactory))
|
||||
image = ImageFactory()
|
||||
key = factory.LazyFunction(generate_course_key)
|
||||
owners = factory.LazyFunction(partial(generate_instances, OrganizationFactory, count=1))
|
||||
|
||||
@@ -16,6 +16,7 @@ from nose.plugins.attrib import attr
|
||||
from pytz import utc
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from entitlements.tests.factories import CourseEntitlementFactory
|
||||
from lms.djangoapps.certificates.api import MODES
|
||||
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
|
||||
from lms.djangoapps.commerce.utils import EcommerceService
|
||||
@@ -23,6 +24,7 @@ from lms.djangoapps.grades.tests.utils import mock_passing_grade
|
||||
from openedx.core.djangoapps.catalog.tests.factories import (
|
||||
CourseFactory,
|
||||
CourseRunFactory,
|
||||
EntitlementFactory,
|
||||
ProgramFactory,
|
||||
SeatFactory,
|
||||
generate_course_run_key
|
||||
@@ -63,7 +65,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
def _create_enrollments(self, *course_run_ids):
|
||||
"""Variadic helper used to create course run enrollments."""
|
||||
for course_run_id in course_run_ids:
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_id, mode='verified')
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_id, mode=CourseMode.VERIFIED)
|
||||
|
||||
def _assert_progress(self, meter, *progresses):
|
||||
"""Variadic helper used to verify progress calculations."""
|
||||
@@ -225,22 +227,22 @@ class TestProgramProgressMeter(TestCase):
|
||||
course_run_key = generate_course_run_key()
|
||||
now = datetime.datetime.now(utc)
|
||||
upgrade_deadline = None if not offset else str(now + datetime.timedelta(days=offset))
|
||||
required_seat = SeatFactory(type='verified', upgrade_deadline=upgrade_deadline)
|
||||
enrolled_seat = SeatFactory(type='audit')
|
||||
required_seat = SeatFactory(type=CourseMode.VERIFIED, upgrade_deadline=upgrade_deadline)
|
||||
enrolled_seat = SeatFactory(type=CourseMode.AUDIT)
|
||||
seats = [required_seat, enrolled_seat]
|
||||
|
||||
data = [
|
||||
ProgramFactory(
|
||||
courses=[
|
||||
CourseFactory(course_runs=[
|
||||
CourseRunFactory(key=course_run_key, type='verified', seats=seats),
|
||||
CourseRunFactory(key=course_run_key, type=CourseMode.VERIFIED, seats=seats),
|
||||
]),
|
||||
]
|
||||
)
|
||||
]
|
||||
mock_get_programs.return_value = data
|
||||
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_key, mode='audit')
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course_run_key, mode=CourseMode.AUDIT)
|
||||
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
|
||||
@@ -537,7 +539,9 @@ class TestProgramProgressMeter(TestCase):
|
||||
Verify that the method can find course run certificates when not mocked out.
|
||||
"""
|
||||
mock_get_certificates_for_user.return_value = [
|
||||
self._make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'),
|
||||
self._make_certificate_result(
|
||||
status='downloadable', type=CourseMode.VERIFIED, course_key='downloadable-course'
|
||||
),
|
||||
self._make_certificate_result(status='generating', type='honor', course_key='generating-course'),
|
||||
self._make_certificate_result(status='unknown', course_key='unknown-course'),
|
||||
]
|
||||
@@ -546,7 +550,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
self.assertEqual(
|
||||
meter.completed_course_runs,
|
||||
[
|
||||
{'course_run_id': 'downloadable-course', 'type': 'verified'},
|
||||
{'course_run_id': 'downloadable-course', 'type': CourseMode.VERIFIED},
|
||||
{'course_run_id': 'generating-course', 'type': 'honor'},
|
||||
]
|
||||
)
|
||||
@@ -558,9 +562,10 @@ class TestProgramProgressMeter(TestCase):
|
||||
Verify that 'no-id-professional' certificates are treated as if they were
|
||||
'professional' certificates when determining program completion.
|
||||
"""
|
||||
# Create serialized course runs like the ones we expect to receive from
|
||||
# the discovery service's API. These runs are of type 'professional'.
|
||||
course_runs = CourseRunFactory.create_batch(2, type='professional')
|
||||
# Create serialized course runs like the ones we expect to receive from the discovery service's API.
|
||||
# These runs are of type 'professional' because there is no seat type for no-id-professional;
|
||||
# it uses professional as the seat type instead.
|
||||
course_runs = CourseRunFactory.create_batch(2, type=CourseMode.PROFESSIONAL)
|
||||
program = ProgramFactory(courses=[CourseFactory(course_runs=course_runs)])
|
||||
mock_get_programs.return_value = [program]
|
||||
|
||||
@@ -571,7 +576,9 @@ class TestProgramProgressMeter(TestCase):
|
||||
# Grant a 'no-id-professional' certificate for one of the course runs,
|
||||
# thereby completing the program.
|
||||
mock_get_certificates_for_user.return_value = [
|
||||
self._make_certificate_result(status='downloadable', type='no-id-professional', course_key=course_runs[0]['key'])
|
||||
self._make_certificate_result(
|
||||
status='downloadable', type=CourseMode.NO_ID_PROFESSIONAL_MODE, course_key=course_runs[0]['key']
|
||||
)
|
||||
]
|
||||
|
||||
# Verify that the program is complete.
|
||||
@@ -592,7 +599,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
mock_get_programs.return_value = [program]
|
||||
self._create_enrollments(course_run_key)
|
||||
meter = ProgramProgressMeter(self.site, self.user)
|
||||
mock_completed_course_runs.return_value = [{'course_run_id': course_run_key, 'type': 'verified'}]
|
||||
mock_completed_course_runs.return_value = [{'course_run_id': course_run_key, 'type': CourseMode.VERIFIED}]
|
||||
self.assertEqual(meter._is_course_complete(course), True)
|
||||
|
||||
def test_course_grade_results(self, mock_get_programs):
|
||||
@@ -628,7 +635,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
self.assertEqual(meter.progress(count_only=False), expected)
|
||||
|
||||
|
||||
def _create_course(self, course_price, course_run_count=1):
|
||||
def _create_course(self, course_price, course_run_count=1, make_entitlement=False):
|
||||
"""
|
||||
Creates the course in mongo and update it with the instructor data.
|
||||
Also creates catalog course with respect to course run.
|
||||
@@ -646,8 +653,9 @@ def _create_course(self, course_price, course_run_count=1):
|
||||
|
||||
run = CourseRunFactory(key=unicode(course.id), seats=[SeatFactory(price=course_price)])
|
||||
course_runs.append(run)
|
||||
entitlements = [EntitlementFactory()] if make_entitlement else []
|
||||
|
||||
return CourseFactory(course_runs=course_runs)
|
||||
return CourseFactory(course_runs=course_runs, entitlements=entitlements)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -879,12 +887,12 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
|
||||
course1 = _create_course(self, self.course_price)
|
||||
course2 = _create_course(self, self.course_price)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='verified')
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course2['course_runs'][0]['key'], mode='audit')
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode=CourseMode.VERIFIED)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course2['course_runs'][0]['key'], mode=CourseMode.AUDIT)
|
||||
program2 = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=['verified'],
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program2, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
@@ -897,12 +905,12 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, course_run_count=2)
|
||||
course2 = _create_course(self, self.course_price)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='verified')
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode=CourseMode.VERIFIED)
|
||||
course1['course_runs'][0]['status'] = 'unpublished'
|
||||
program2 = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=['verified'],
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program2, self.user).extend()
|
||||
self.assertEqual(len(data['skus']), 1)
|
||||
@@ -915,12 +923,13 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
This test is primarily for the case of no-id-professional enrollment modes
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode='no-id-professional')
|
||||
CourseEnrollmentFactory(
|
||||
user=self.user, course_id=course1['course_runs'][0]['key'], mode=CourseMode.NO_ID_PROFESSIONAL_MODE
|
||||
)
|
||||
program2 = ProgramFactory(
|
||||
courses=[course1],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=['professional'], # There is no seat type for no-id-professional, it
|
||||
# instead uses professional
|
||||
applicable_seat_types=[CourseMode.PROFESSIONAL]
|
||||
)
|
||||
data = ProgramDataExtender(program2, self.user).extend()
|
||||
self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
|
||||
@@ -938,7 +947,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
key=str(ModuleStoreCourseFactory().id),
|
||||
status='published'
|
||||
)
|
||||
course = CourseFactory(course_runs=[course_run_1, course_run_2])
|
||||
course = CourseFactory(course_runs=[course_run_1, course_run_2], entitlements=[])
|
||||
program = ProgramFactory(
|
||||
courses=[
|
||||
CourseFactory(course_runs=[
|
||||
@@ -956,7 +965,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
])
|
||||
],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=['verified']
|
||||
applicable_seat_types=[CourseMode.VERIFIED]
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
|
||||
@@ -967,6 +976,147 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
|
||||
def test_learner_eligibility_for_one_click_purchase_entitlement_products(self):
|
||||
"""
|
||||
Learner should be eligible for one click purchase if:
|
||||
- program is eligible for one click purchase
|
||||
- There are remaining unpurchased courses with entitlement products
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
course2 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
expected_skus = set([course1['entitlements'][0]['sku'], course2['entitlements'][0]['sku']])
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(set(data['skus']), expected_skus)
|
||||
|
||||
def test_learner_eligibility_for_one_click_purchase_ineligible_program(self):
|
||||
"""
|
||||
Learner should not be eligible for one click purchase if the program is not eligible for one click purchase
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
course2 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=False,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(data['skus'], [])
|
||||
|
||||
def test_learner_eligibility_for_one_click_purchase_user_entitlements(self):
|
||||
"""
|
||||
Learner should be eligibile for one click purchase if they hold an entitlement in one or more courses
|
||||
in the program and there are remaining unpurchased courses in the program with entitlement products.
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
course2 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
CourseEntitlementFactory(user=self.user, course_uuid=course1['uuid'], mode=CourseMode.VERIFIED)
|
||||
expected_skus = set([course2['entitlements'][0]['sku']])
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(set(data['skus']), expected_skus)
|
||||
|
||||
def test_all_courses_owned(self):
|
||||
"""
|
||||
Learner should not be eligible for one click purchase if they hold entitlements in all courses in the program.
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, make_entitlement=True)
|
||||
course2 = _create_course(self, self.course_price)
|
||||
CourseEntitlementFactory(user=self.user, course_uuid=course1['uuid'], mode=CourseMode.VERIFIED)
|
||||
CourseEntitlementFactory(user=self.user, course_uuid=course2['uuid'], mode=CourseMode.VERIFIED)
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(data['skus'], [])
|
||||
|
||||
def test_entitlement_product_wrong_mode(self):
|
||||
"""
|
||||
Learner should not be eligible for one click purchase if the only entitlement product
|
||||
for a course in the program is not in an applicable mode, and that course has multiple course runs.
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price)
|
||||
course2 = _create_course(self, self.course_price, course_run_count=2)
|
||||
course2['entitlements'].append(EntitlementFactory(mode=CourseMode.PROFESSIONAL))
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertFalse(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(data['skus'], [])
|
||||
|
||||
def test_second_entitlement_product_wrong_mode(self):
|
||||
"""
|
||||
Learner should be eligible for one click purchase if a course has multiple entitlement products
|
||||
and at least one of them is in an applicable mode, even if one is not in an applicable mode.
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price)
|
||||
course2 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
# The above statement makes a verfied entitlement for the course, which is an applicable seat type
|
||||
# and the statement below makes a professional entitlement for the same course, which is not applicable
|
||||
course2['entitlements'].append(EntitlementFactory(mode=CourseMode.PROFESSIONAL))
|
||||
expected_skus = set([course1['course_runs'][0]['seats'][0]['sku'], course2['entitlements'][0]['sku']])
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(set(data['skus']), expected_skus)
|
||||
|
||||
def test_entitlement_product_and_user_enrollment(self):
|
||||
"""
|
||||
Learner should be eligible for one click purchase if they hold an enrollment
|
||||
but not an entitlement in a course for which there exists an entitlement product.
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, make_entitlement=True)
|
||||
course2 = _create_course(self, self.course_price)
|
||||
expected_skus = set([course2['course_runs'][0]['seats'][0]['sku']])
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode=CourseMode.VERIFIED)
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(set(data['skus']), expected_skus)
|
||||
|
||||
def test_user_enrollment_with_other_course_entitlement_product(self):
|
||||
"""
|
||||
Learner should be eligible for one click purchase if they hold an enrollment in one course of the program
|
||||
and there is an entitlement product for another course in the program.
|
||||
"""
|
||||
course1 = _create_course(self, self.course_price, course_run_count=2)
|
||||
course2 = _create_course(self, self.course_price, course_run_count=2, make_entitlement=True)
|
||||
CourseEnrollmentFactory(user=self.user, course_id=course1['course_runs'][0]['key'], mode=CourseMode.VERIFIED)
|
||||
expected_skus = set([course2['entitlements'][0]['sku']])
|
||||
program = ProgramFactory(
|
||||
courses=[course1, course2],
|
||||
is_program_eligible_for_one_click_purchase=True,
|
||||
applicable_seat_types=[CourseMode.VERIFIED, CourseMode.PROFESSIONAL],
|
||||
)
|
||||
data = ProgramDataExtender(program, self.user).extend()
|
||||
self.assertTrue(data['is_learner_eligible_for_one_click_purchase'])
|
||||
self.assertEqual(set(data['skus']), expected_skus)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@mock.patch(UTILS_MODULE + '.get_credentials')
|
||||
@@ -1095,7 +1245,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
self.number_of_courses = 2
|
||||
self.program = ProgramFactory(
|
||||
courses=[_create_course(self, self.course_price) for __ in range(self.number_of_courses)],
|
||||
applicable_seat_types=['verified']
|
||||
applicable_seat_types=[CourseMode.VERIFIED]
|
||||
)
|
||||
|
||||
def _prepare_program_for_discounted_price_calculation_endpoint(self):
|
||||
@@ -1212,8 +1362,9 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase):
|
||||
body=json.dumps(mock_discount_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
user = AnonymousUserFactory()
|
||||
|
||||
data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend()
|
||||
data = ProgramMarketingDataExtender(self.program, user).extend()
|
||||
self._update_discount_data(mock_discount_data)
|
||||
|
||||
self.assertEqual(
|
||||
|
||||
@@ -460,57 +460,99 @@ class ProgramDataExtender(object):
|
||||
def _attach_course_run_may_certify(self, run_mode):
|
||||
run_mode['may_certify'] = self.course_overview.may_certify()
|
||||
|
||||
def _check_enrollment_for_user(self, course_run):
|
||||
applicable_seat_types = self.data['applicable_seat_types']
|
||||
def _filter_out_courses_with_entitlements(self, courses):
|
||||
"""
|
||||
Removes courses for which the current user already holds an applicable entitlement.
|
||||
|
||||
(enrollment_mode, active) = CourseEnrollment.enrollment_mode_for_user(
|
||||
self.user,
|
||||
CourseKey.from_string(course_run['key'])
|
||||
TODO:
|
||||
Add a NULL value of enrollment_course_run to filter, as courses with entitlements spent on applicable
|
||||
enrollments will already have been filtered out by _filter_out_courses_with_enrollments.
|
||||
|
||||
Arguments:
|
||||
courses (list): Containing dicts representing courses in a program
|
||||
|
||||
Returns:
|
||||
A subset of the given list of course dicts
|
||||
"""
|
||||
course_uuids = set(course['uuid'] for course in courses)
|
||||
# Filter the entitlements' modes with a case-insensitive match against applicable seat_types
|
||||
entitlements = self.user.courseentitlement_set.filter(
|
||||
mode__in=self.data['applicable_seat_types'],
|
||||
course_uuid__in=course_uuids,
|
||||
)
|
||||
# Here we check the entitlements' expired_at_datetime property rather than filter by the expired_at attribute
|
||||
# to ensure that the expiration status is as up to date as possible
|
||||
entitlements = [e for e in entitlements if not e.expired_at_datetime]
|
||||
courses_with_entitlements = set(unicode(entitlement.course_uuid) for entitlement in entitlements)
|
||||
return [course for course in courses if course['uuid'] not in courses_with_entitlements]
|
||||
|
||||
is_paid_seat = False
|
||||
if enrollment_mode is not None and active is not None and active is True:
|
||||
# Check all the applicable seat types
|
||||
# this will also check for no-id-professional as professional
|
||||
is_paid_seat = any(seat_type in enrollment_mode for seat_type in applicable_seat_types)
|
||||
def _filter_out_courses_with_enrollments(self, courses):
|
||||
"""
|
||||
Removes courses for which the current user already holds an active and applicable enrollment
|
||||
for one of that course's runs.
|
||||
|
||||
return is_paid_seat
|
||||
Arguments:
|
||||
courses (list): Containing dicts representing courses in a program
|
||||
|
||||
Returns:
|
||||
A subset of the given list of course dicts
|
||||
"""
|
||||
enrollments = self.user.courseenrollment_set.filter(
|
||||
is_active=True,
|
||||
mode__in=self.data['applicable_seat_types']
|
||||
)
|
||||
course_runs_with_enrollments = set(unicode(enrollment.course_id) for enrollment in enrollments)
|
||||
courses_without_enrollments = []
|
||||
for course in courses:
|
||||
if all(unicode(run['key']) not in course_runs_with_enrollments for run in course['course_runs']):
|
||||
courses_without_enrollments.append(course)
|
||||
|
||||
return courses_without_enrollments
|
||||
|
||||
def _collect_one_click_purchase_eligibility_data(self):
|
||||
"""
|
||||
Extend the program data with data about learner's eligibility for one click purchase,
|
||||
discount data of the program and SKUs of seats that should be added to basket.
|
||||
"""
|
||||
applicable_seat_types = self.data['applicable_seat_types']
|
||||
if 'professional' in self.data['applicable_seat_types']:
|
||||
self.data['applicable_seat_types'].append('no-id-professional')
|
||||
applicable_seat_types = set(seat for seat in self.data['applicable_seat_types'] if seat != 'credit')
|
||||
|
||||
is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
|
||||
skus = []
|
||||
bundle_variant = 'full'
|
||||
|
||||
if is_learner_eligible_for_one_click_purchase:
|
||||
for course in self.data['courses']:
|
||||
add_course_sku = True
|
||||
course_runs = course.get('course_runs', [])
|
||||
published_course_runs = filter(lambda run: run['status'] == 'published', course_runs)
|
||||
courses = self.data['courses']
|
||||
if not self.user.is_anonymous():
|
||||
courses = self._filter_out_courses_with_enrollments(courses)
|
||||
courses = self._filter_out_courses_with_entitlements(courses)
|
||||
|
||||
if len(published_course_runs) == 1:
|
||||
for course_run in course_runs:
|
||||
is_paid_seat = self._check_enrollment_for_user(course_run)
|
||||
if len(courses) < len(self.data['courses']):
|
||||
bundle_variant = 'partial'
|
||||
|
||||
if is_paid_seat:
|
||||
add_course_sku = False
|
||||
break
|
||||
|
||||
if add_course_sku:
|
||||
for course in courses:
|
||||
entitlement_product = False
|
||||
for entitlement in course.get('entitlements', []):
|
||||
# We add the first entitlement product found with an applicable seat type because, at this time,
|
||||
# we are assuming that, for any given course, there is at most one paid entitlement available.
|
||||
if entitlement['mode'] in applicable_seat_types:
|
||||
skus.append(entitlement['sku'])
|
||||
entitlement_product = True
|
||||
break
|
||||
if not entitlement_product:
|
||||
course_runs = course.get('course_runs', [])
|
||||
published_course_runs = [run for run in course_runs if run['status'] == 'published']
|
||||
if len(published_course_runs) == 1:
|
||||
for seat in published_course_runs[0]['seats']:
|
||||
if seat['type'] in applicable_seat_types and seat['sku']:
|
||||
skus.append(seat['sku'])
|
||||
break
|
||||
else:
|
||||
bundle_variant = 'partial'
|
||||
else:
|
||||
# If a course in the program has more than 1 published course run
|
||||
# learner won't be eligible for a one click purchase.
|
||||
is_learner_eligible_for_one_click_purchase = False
|
||||
skus = []
|
||||
break
|
||||
# If a course in the program has more than 1 published course run
|
||||
# learner won't be eligible for a one click purchase.
|
||||
skus = []
|
||||
break
|
||||
|
||||
if skus:
|
||||
try:
|
||||
@@ -604,7 +646,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
|
||||
def __init__(self, program_data, user):
|
||||
super(ProgramMarketingDataExtender, self).__init__(program_data, user)
|
||||
|
||||
# Aggregate list of instructors for the program
|
||||
# Aggregate list of instructors for the program keyed by name
|
||||
self.instructors = []
|
||||
|
||||
# Values for programs' price calculation.
|
||||
|
||||
Reference in New Issue
Block a user