Unverified Commit b49939fb by McKenzie Welter Committed by GitHub

Merge pull request #16619 from edx/WIP-lms-bundling-button

Program Details Page one-click purchase button with entitlements
parents c25e4ba0 f72cf800
...@@ -136,10 +136,10 @@ class CourseMode(models.Model): ...@@ -136,10 +136,10 @@ class CourseMode(models.Model):
HONOR = 'honor' HONOR = 'honor'
PROFESSIONAL = 'professional' PROFESSIONAL = 'professional'
VERIFIED = "verified" VERIFIED = 'verified'
AUDIT = "audit" AUDIT = 'audit'
NO_ID_PROFESSIONAL_MODE = "no-id-professional" NO_ID_PROFESSIONAL_MODE = 'no-id-professional'
CREDIT_MODE = "credit" CREDIT_MODE = 'credit'
DEFAULT_MODE = Mode( DEFAULT_MODE = Mode(
settings.COURSE_MODE_DEFAULTS['slug'], settings.COURSE_MODE_DEFAULTS['slug'],
......
...@@ -24,3 +24,10 @@ class CourseEntitlement(TimeStampedModel): ...@@ -24,3 +24,10 @@ class CourseEntitlement(TimeStampedModel):
help_text='The current Course enrollment for this entitlement. If NULL the Learner has not enrolled.' 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) 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 ...@@ -8,6 +8,7 @@ from faker import Faker
fake = Faker() fake = Faker()
VERIFIED_MODE = 'verified'
def generate_instances(factory_class, count=3): def generate_instances(factory_class, count=3):
...@@ -103,10 +104,18 @@ class SeatFactory(DictFactoryBase): ...@@ -103,10 +104,18 @@ class SeatFactory(DictFactoryBase):
currency = 'USD' currency = 'USD'
price = factory.Faker('random_int') price = factory.Faker('random_int')
sku = factory.LazyFunction(generate_seat_sku) sku = factory.LazyFunction(generate_seat_sku)
type = 'verified' type = VERIFIED_MODE
upgrade_deadline = factory.LazyFunction(generate_zulu_datetime) 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): class CourseRunFactory(DictFactoryBase):
eligible_for_financial_aid = True eligible_for_financial_aid = True
end = factory.LazyFunction(generate_zulu_datetime) end = factory.LazyFunction(generate_zulu_datetime)
...@@ -121,7 +130,7 @@ class CourseRunFactory(DictFactoryBase): ...@@ -121,7 +130,7 @@ class CourseRunFactory(DictFactoryBase):
start = factory.LazyFunction(generate_zulu_datetime) start = factory.LazyFunction(generate_zulu_datetime)
status = 'published' status = 'published'
title = factory.Faker('catch_phrase') title = factory.Faker('catch_phrase')
type = 'verified' type = VERIFIED_MODE
uuid = factory.Faker('uuid4') uuid = factory.Faker('uuid4')
content_language = 'en' content_language = 'en'
max_effort = 4 max_effort = 4
...@@ -130,6 +139,7 @@ class CourseRunFactory(DictFactoryBase): ...@@ -130,6 +139,7 @@ class CourseRunFactory(DictFactoryBase):
class CourseFactory(DictFactoryBase): class CourseFactory(DictFactoryBase):
course_runs = factory.LazyFunction(partial(generate_instances, CourseRunFactory)) course_runs = factory.LazyFunction(partial(generate_instances, CourseRunFactory))
entitlements = factory.LazyFunction(partial(generate_instances, EntitlementFactory))
image = ImageFactory() image = ImageFactory()
key = factory.LazyFunction(generate_course_key) key = factory.LazyFunction(generate_course_key)
owners = factory.LazyFunction(partial(generate_instances, OrganizationFactory, count=1)) owners = factory.LazyFunction(partial(generate_instances, OrganizationFactory, count=1))
......
...@@ -460,55 +460,97 @@ class ProgramDataExtender(object): ...@@ -460,55 +460,97 @@ class ProgramDataExtender(object):
def _attach_course_run_may_certify(self, run_mode): def _attach_course_run_may_certify(self, run_mode):
run_mode['may_certify'] = self.course_overview.may_certify() run_mode['may_certify'] = self.course_overview.may_certify()
def _check_enrollment_for_user(self, course_run): def _filter_out_courses_with_entitlements(self, courses):
applicable_seat_types = self.data['applicable_seat_types'] """
Removes courses for which the current user already holds an applicable entitlement.
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
(enrollment_mode, active) = CourseEnrollment.enrollment_mode_for_user( Returns:
self.user, A subset of the given list of course dicts
CourseKey.from_string(course_run['key']) """
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]
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.
is_paid_seat = False Arguments:
if enrollment_mode is not None and active is not None and active is True: courses (list): Containing dicts representing courses in a program
# 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)
return is_paid_seat 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): def _collect_one_click_purchase_eligibility_data(self):
""" """
Extend the program data with data about learner's eligibility for one click purchase, 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. 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'] is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
skus = [] skus = []
bundle_variant = 'full' bundle_variant = 'full'
if is_learner_eligible_for_one_click_purchase: if is_learner_eligible_for_one_click_purchase:
for course in self.data['courses']: courses = self.data['courses']
add_course_sku = True if not self.user.is_anonymous():
course_runs = course.get('course_runs', []) courses = self._filter_out_courses_with_enrollments(courses)
published_course_runs = filter(lambda run: run['status'] == 'published', course_runs) courses = self._filter_out_courses_with_entitlements(courses)
if len(published_course_runs) == 1: if len(courses) < len(self.data['courses']):
for course_run in course_runs: bundle_variant = 'partial'
is_paid_seat = self._check_enrollment_for_user(course_run)
if is_paid_seat: for course in courses:
add_course_sku = False 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 break
if not entitlement_product:
if add_course_sku: 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']: for seat in published_course_runs[0]['seats']:
if seat['type'] in applicable_seat_types and seat['sku']: if seat['type'] in applicable_seat_types and seat['sku']:
skus.append(seat['sku']) skus.append(seat['sku'])
else: break
bundle_variant = 'partial'
else: else:
# If a course in the program has more than 1 published course run # If a course in the program has more than 1 published course run
# learner won't be eligible for a one click purchase. # learner won't be eligible for a one click purchase.
is_learner_eligible_for_one_click_purchase = False
skus = [] skus = []
break break
...@@ -604,7 +646,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender): ...@@ -604,7 +646,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender):
def __init__(self, program_data, user): def __init__(self, program_data, user):
super(ProgramMarketingDataExtender, self).__init__(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 = [] self.instructors = []
# Values for programs' price calculation. # Values for programs' price calculation.
......
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