Skip to content
Snippets Groups Projects
Commit b2432ac5 authored by Christie Rice's avatar Christie Rice
Browse files

REVEM-207 Add program price and purchase url to user metadata

parent 0c06bd3c
No related merge requests found
"""
Tests of experiment functionality
"""
from decimal import Decimal
from unittest import TestCase
from lms.djangoapps.experiments.utils import is_enrolled_in_course_run
from lms.djangoapps.experiments.utils import get_course_entitlement_price_and_sku, get_program_price_and_skus, \
get_program_purchase_url, get_unenrolled_courses, is_enrolled_in_course_run
from opaque_keys.edx.keys import CourseKey
......@@ -10,6 +12,22 @@ class ExperimentUtilsTests(TestCase):
"""
Tests of experiment functionality
"""
def setUp(self):
super(ExperimentUtilsTests, self).setUp()
# Create a course run
self.run_a_price = '86.00'
self.run_a_sku = 'B9B6D0B'
seat_a = {'type': 'verified', 'price': self.run_a_price, 'sku': self.run_a_sku}
seats = [seat_a]
self.course_run_a = {'status': 'published', 'seats': seats}
# Create an entitlement
self.entitlement_a_price = '199.23'
self.entitlement_a_sku = 'B37EBA0'
self.entitlement_a = {'mode': 'verified', 'price': self.entitlement_a_price, 'sku': self.entitlement_a_sku}
def test_valid_course_run_key_enrollment(self):
course_run = {
'key': 'course-v1:DelftX+NGIx+RA0',
......@@ -23,3 +41,104 @@ class ExperimentUtilsTests(TestCase):
}
enrollment_ids = {CourseKey.from_string('course-v1:DelftX+NGIx+RA0')}
self.assertFalse(is_enrolled_in_course_run(course_run, enrollment_ids))
def test_program_url_with_no_skus(self):
url = get_program_purchase_url(None)
self.assertEqual(None, url)
def test_program_url_with_no_skus_and_no_bundle(self):
url = get_program_purchase_url(None, None)
self.assertEqual(None, url)
def test_program_url_with_single_sku(self):
skus = ['9FE0DE2']
expected_url = 'https://ecommerce.edx.org/basket/add/?sku=9FE0DE2'
url = get_program_purchase_url(skus)
self.assertEqual(expected_url, url)
def test_program_url_with_single_sku_and_bundle(self):
skus = ['9FE0DE2']
program_id = 'bef7201a-6f97-40ad-ad17-d5ea8be1eec8'
expected_url = 'https://ecommerce.edx.org/basket/add/?sku=9FE0DE2&bundle=' + program_id
url = get_program_purchase_url(skus, program_id)
self.assertEqual(expected_url, url)
def test_program_url_with_multiple_skus(self):
skus = ['9FE0DE2', 'B37EBA0', 'FDCED11']
expected_url = 'https://ecommerce.edx.org/basket/add/?sku=9FE0DE2&sku=B37EBA0&sku=FDCED11'
url = get_program_purchase_url(skus)
self.assertEqual(expected_url, url)
def test_program_url_with_multiple_skus_and_bundle(self):
skus = ['9FE0DE2', 'B37EBA0', 'FDCED11']
program_id = 'bef7201a-6f97-40ad-ad17-d5ea8be1eec8'
expected_url = 'https://ecommerce.edx.org/basket/add/?sku=9FE0DE2&sku=B37EBA0&sku=FDCED11&bundle=' + program_id
url = get_program_purchase_url(skus, program_id)
self.assertEqual(expected_url, url)
def test_program_price_and_skus_for_empty_courses(self):
price, skus = get_program_price_and_skus([])
self.assertEqual(None, price)
self.assertEqual(None, skus)
def test_unenrolled_courses_for_empty_courses(self):
unenrolled_courses = get_unenrolled_courses([], [])
self.assertEqual([], unenrolled_courses)
def test_unenrolled_courses_for_single_course(self):
course = {'key': 'UQx+ENGY1x'}
courses_in_program = [course]
user_enrollments = []
unenrolled_courses = get_unenrolled_courses(courses_in_program, user_enrollments)
expected_unenrolled_courses = [course]
self.assertEqual(expected_unenrolled_courses, unenrolled_courses)
def test_price_and_sku_from_empty_course(self):
course = {}
price, sku = get_course_entitlement_price_and_sku(course)
self.assertEqual(None, price)
self.assertEqual(None, sku)
def test_price_and_sku_from_entitlement(self):
entitlements = [self.entitlement_a]
course = {'key': 'UQx+ENGY1x', 'entitlements': entitlements}
price, sku = get_course_entitlement_price_and_sku(course)
self.assertEqual(self.entitlement_a_price, price)
self.assertEqual(self.entitlement_a_sku, sku)
def test_price_and_sku_from_course_run(self):
course_runs = [self.course_run_a]
course = {'key': 'UQx+ENGY1x', 'course_runs': course_runs}
price, sku = get_course_entitlement_price_and_sku(course)
expected_price = Decimal(self.run_a_price)
self.assertEqual(expected_price, price)
self.assertEqual(self.run_a_sku, sku)
def test_price_and_sku_from_course(self):
entitlements = [self.entitlement_a]
course_a = {'key': 'UQx+ENGYCAPx', 'entitlements': entitlements}
courses = [course_a]
price, skus = get_program_price_and_skus(courses)
expected_price = u'$199.23'
self.assertEqual(expected_price, price)
self.assertEqual(1, len(skus))
self.assertIn(self.entitlement_a_sku, skus)
def test_price_and_sku_from_multiple_courses(self):
entitlements = [self.entitlement_a]
course_runs = [self.course_run_a]
course_a = {'key': 'UQx+ENGY1x', 'course_runs': course_runs}
course_b = {'key': 'UQx+ENGYCAPx', 'entitlements': entitlements}
courses = [course_a, course_b]
price, skus = get_program_price_and_skus(courses)
expected_price = u'$285.23'
self.assertEqual(expected_price, price)
self.assertEqual(2, len(skus))
self.assertIn(self.run_a_sku, skus)
self.assertIn(self.entitlement_a_sku, skus)
......@@ -5,9 +5,11 @@ Utilities to facilitate experimentation
import hashlib
import re
import logging
from decimal import Decimal
from student.models import CourseEnrollment
from django_comment_common.models import Role
from course_modes.models import get_cosmetic_verified_display_price
from django.utils.timezone import now
from course_modes.models import get_cosmetic_verified_display_price, format_course_price
from courseware.access import has_staff_access_to_preview_mode
from courseware.date_summary import verified_upgrade_deadline_link, verified_upgrade_link_is_valid
from xmodule.partitions.partitions_service import get_user_partition_groups, get_all_partitions_for_course
......@@ -37,6 +39,24 @@ PROGRAM_INFO_FLAG = WaffleFlag(
flag_name=u'add_programs',
flag_undefined_default=False
)
# .. feature_toggle_name: experiments.add_program_price
# .. feature_toggle_type: flag
# .. feature_toggle_default: False
# .. feature_toggle_description: Toggle for adding the current course's program price and sku information to user
# metadata
# .. feature_toggle_category: experiments
# .. feature_toggle_use_cases: monitored_rollout
# .. feature_toggle_creation_date: 2019-3-12
# .. feature_toggle_expiration_date: None
# .. feature_toggle_warnings: None
# .. feature_toggle_tickets: REVEM-118, REVEM-206
# .. feature_toggle_status: supported
PROGRAM_PRICE_FLAG = WaffleFlag(
waffle_namespace=WaffleFlagNamespace(name=u'experiments'),
flag_name=u'add_program_price',
flag_undefined_default=False
)
# TODO: clean up as part of REVEM-199 (END)
......@@ -78,14 +98,96 @@ def check_and_get_upgrade_link_and_date(user, enrollment=None, course=None):
# TODO: clean up as part of REVEM-199 (START)
def is_enrolled_in_all_courses_in_program(courses_in_program, user_enrollments):
def get_program_purchase_url(skus, bundle_id=None):
"""
Return a url that will allow the purchase of the courses with these skus
"""
if not skus:
return None
url = 'https://ecommerce.edx.org/basket/add/?'
for sku in skus:
url += 'sku=' + sku + '&'
if bundle_id:
url += 'bundle=' + bundle_id
else:
# Remove trailing & from the skus
url = url[:-1]
return url
def get_program_price_and_skus(courses):
"""
Get the total program price and purchase skus from these courses in the program
"""
program_price = 0
skus = []
for course in courses:
course_price, course_sku = get_course_entitlement_price_and_sku(course)
if course_price is not None and course_sku is not None:
program_price = Decimal(program_price) + Decimal(course_price)
skus.append(course_sku)
if program_price <= 0:
program_price = None
skus = None
else:
program_price = format_course_price(program_price)
program_price = unicode(program_price)
return program_price, skus
def get_course_entitlement_price_and_sku(course):
"""
Get the entitlement price and sku from this course.
Try to get them from the first non-expired, verified entitlement that has a price and a sku. If that doesn't work,
fall back to the first non-expired, verified course run that has a price and a sku.
"""
for entitlement in course.get('entitlements', []):
if entitlement.get('mode') == 'verified' and entitlement['price'] and entitlement['sku']:
expires = entitlement.get('expires')
if not expires or expires > now():
return entitlement['price'], entitlement['sku']
course_runs = course.get('course_runs', [])
published_course_runs = [run for run in course_runs if run['status'] == 'published']
for published_course_run in published_course_runs:
for seat in published_course_run['seats']:
if seat.get('type') == 'verified' and seat['price'] and seat['sku']:
price = Decimal(seat.get('price'))
return price, seat.get('sku')
return None, None
def get_unenrolled_courses(courses, user_enrollments):
"""
Given a list of courses and a list of user enrollments, return the courses in which the user is not enrolled.
Depending on the enrollments that are passed in, this method can be used to determine the courses in a program in
which the user has not yet enrolled or the courses in a program for which the user has not yet purchased a
certificate.
"""
# Get the enrollment course ids here, so we don't need to loop through them for every course run
enrollment_course_ids = {enrollment.course_id for enrollment in user_enrollments}
unenrolled_courses = []
for course in courses:
if not is_enrolled_in_course(course, enrollment_course_ids):
unenrolled_courses.append(course)
return unenrolled_courses
def is_enrolled_in_all_courses(courses, user_enrollments):
"""
Determine if the user is enrolled in all courses in this program
Determine if the user is enrolled in all of the courses
"""
# Get the enrollment course ids here, so we don't need to loop through them for every course run
enrollment_course_ids = {enrollment.course_id for enrollment in user_enrollments}
for course in courses_in_program:
for course in courses:
if not is_enrolled_in_course(course, enrollment_course_ids):
# User is not enrolled in this course, meaning they are not enrolled in all courses in the program
return False
......@@ -148,18 +250,40 @@ def get_experiment_user_metadata_context(course, user):
# A course can be in multiple programs, but we're just grabbing the first one
program = programs[0]
complete_enrollment = False
has_courses_left_to_purchase = False
total_courses = None
courses = program.get('courses')
courses_left_to_purchase_price = None
courses_left_to_purchase_url = None
program_uuid = program.get('uuid')
if courses is not None:
total_courses = len(courses)
complete_enrollment = is_enrolled_in_all_courses_in_program(courses, user_enrollments)
complete_enrollment = is_enrolled_in_all_courses(courses, user_enrollments)
if PROGRAM_PRICE_FLAG.is_enabled():
# Get the price and purchase URL of the program courses the user has yet to purchase. Say a
# program has 3 courses (A, B and C), and the user previously purchased a certificate for A.
# The user is enrolled in audit mode for B. The "left to purchase price" should be the price of
# B+C.
non_audit_enrollments = [enrollment for enrollment in user_enrollments if enrollment not in
audit_enrollments]
courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments)
if courses_left_to_purchase:
has_courses_left_to_purchase = True
courses_left_to_purchase_price, courses_left_to_purchase_skus = get_program_price_and_skus(
courses_left_to_purchase)
courses_left_to_purchase_url = get_program_purchase_url(courses_left_to_purchase_skus,
program_uuid)
program_key = {
'uuid': program.get('uuid'),
'uuid': program_uuid,
'title': program.get('title'),
'marketing_url': program.get('marketing_url'),
'total_courses': total_courses,
'complete_enrollment': complete_enrollment,
'has_courses_left_to_purchase': has_courses_left_to_purchase,
'courses_left_to_purchase_price': courses_left_to_purchase_price,
'courses_left_to_purchase_url': courses_left_to_purchase_url,
}
# TODO: clean up as part of REVEM-199 (END)
enrollment = CourseEnrollment.objects.select_related(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment