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):
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))
......
......@@ -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.
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.
(enrollment_mode, active) = CourseEnrollment.enrollment_mode_for_user(
self.user,
CourseKey.from_string(course_run['key'])
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)
if len(published_course_runs) == 1:
for course_run in course_runs:
is_paid_seat = self._check_enrollment_for_user(course_run)
if is_paid_seat:
add_course_sku = False
break
if add_course_sku:
if is_learner_eligible_for_one_click_purchase:
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(courses) < len(self.data['courses']):
bundle_variant = 'partial'
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.
......
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