- 15% off your first upgrade. Discount automatically applied.
'''
+ banner = u'''
'''
button = u''''''
- self.assertContains(response, bannerText, html=True)
+ self.assertContains(response, banner)
self.assertContains(response, button, html=True)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
diff --git a/lms/static/sass/features/_course-experience.scss b/lms/static/sass/features/_course-experience.scss
index baf6a6e0c8..23ed9e204c 100644
--- a/lms/static/sass/features/_course-experience.scss
+++ b/lms/static/sass/features/_course-experience.scss
@@ -33,6 +33,11 @@
color: #23419F;
}
+ a {
+ color: #00496f;
+ text-decoration: underline;
+ font-weight: bold;
+ }
}
// Course call to action message
diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss
index 0053273534..9624b84803 100644
--- a/lms/static/sass/views/_verification.scss
+++ b/lms/static/sass/views/_verification.scss
@@ -2261,11 +2261,19 @@
font-size: 16px;
border-radius: 7px;
padding: 20px;
+ line-height: 1.5;
.first-purchase-offer-banner-bold {
font-weight: bold;
color: #23419f;
}
+
+ a {
+ color: #00496f;
+ text-decoration: underline !important;
+ font-weight: bold;
+ border-bottom: none;
+ }
}
}
diff --git a/openedx/features/course_experience/tests/views/test_course_home.py b/openedx/features/course_experience/tests/views/test_course_home.py
index ad0f0083ce..90de4d8c70 100644
--- a/openedx/features/course_experience/tests/views/test_course_home.py
+++ b/openedx/features/course_experience/tests/views/test_course_home.py
@@ -25,6 +25,7 @@ from experiments.models import ExperimentData
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.course_goals.api import add_course_goal, remove_course_goal
+from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
from lms.djangoapps.courseware.tests.factories import (
BetaTesterFactory,
GlobalStaffFactory,
@@ -34,6 +35,8 @@ from lms.djangoapps.courseware.tests.factories import (
StaffFactory
)
from lms.djangoapps.discussion.django_comment_client.tests.factories import RoleFactory
+from openedx.features.discounts.applicability import get_discount_expiration_date
+from openedx.features.discounts.utils import format_strikeout_price
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
from openedx.core.djangoapps.django_comment_common.models import (
@@ -217,7 +220,7 @@ class TestCourseHomePage(CourseHomePageTestCase):
# Fetch the view and verify the query counts
# TODO: decrease query count as part of REVO-28
- with self.assertNumQueries(95, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
+ with self.assertNumQueries(97, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST):
with check_mongo_calls(4):
url = course_home_url(self.course)
self.client.get(url)
@@ -432,11 +435,19 @@ class TestCourseHomePageAccess(CourseHomePageTestCase):
self.client.login(username=user.username, password=self.TEST_PASSWORD)
url = course_home_url(self.course)
response = self.client.get(url)
+ discount_expiration_date = get_discount_expiration_date(user, self.course).strftime(u'%B %d')
+ upgrade_link = verified_upgrade_deadline_link(user=user, course=self.course)
bannerText = u'''
-
- {}% off your first upgrade.
- Discount automatically applied.
-
'''.format(percentage)
+
+ Upgrade by {discount_expiration_date} and save {percentage}% [{strikeout_price}]
+ Discount will be automatically applied at checkout. Upgrade Now
+
'''.format(
+ discount_expiration_date=discount_expiration_date,
+ percentage=percentage,
+ strikeout_price=HTML(format_strikeout_price(user, self.course, check_for_discount=False)[0]),
+ upgrade_link=upgrade_link
+ )
+
if applicability:
self.assertContains(response, bannerText, html=True)
else:
diff --git a/openedx/features/course_experience/utils.py b/openedx/features/course_experience/utils.py
index f3b2a4fbf0..5baa5946b3 100644
--- a/openedx/features/course_experience/utils.py
+++ b/openedx/features/course_experience/utils.py
@@ -11,9 +11,15 @@ from web_fragments.fragment import Fragment
from lms.djangoapps.course_api.blocks.api import get_blocks
from lms.djangoapps.course_blocks.utils import get_student_module_as_dict
+from lms.djangoapps.courseware.date_summary import verified_upgrade_deadline_link
from openedx.core.djangolib.markup import HTML
from openedx.core.lib.cache_utils import request_cached
-from openedx.features.discounts.applicability import can_receive_discount, discount_percentage
+from openedx.features.discounts.applicability import (
+ can_receive_discount,
+ get_discount_expiration_date,
+ discount_percentage
+)
+from openedx.features.discounts.utils import format_strikeout_price
from xmodule.modulestore.django import modulestore
@@ -192,16 +198,27 @@ def get_resume_block(block):
def get_first_purchase_offer_banner_fragment(user, course):
- if user and course and can_receive_discount(user=user, course=course):
- # Translator: xgettext:no-python-format
- offer_message = _(u'{banner_open}{percentage}% off your first upgrade.{span_close}'
- u' Discount automatically applied.{div_close}')
- return Fragment(HTML(offer_message).format(
- banner_open=HTML(
- '
'),
+ strikeout_price=HTML(format_strikeout_price(user, course, check_for_discount=False)[0])
+ ))
return None
diff --git a/openedx/features/discounts/applicability.py b/openedx/features/discounts/applicability.py
index 425ed63cfa..310bf3a34b 100644
--- a/openedx/features/discounts/applicability.py
+++ b/openedx/features/discounts/applicability.py
@@ -10,9 +10,10 @@ not other discounts like coupons or enterprise/program offers configured in ecom
"""
from __future__ import absolute_import
-from datetime import datetime
+from datetime import datetime, timedelta
from crum import get_current_request, impersonate
+from django.utils import timezone
import pytz
from course_modes.models import CourseMode
@@ -43,7 +44,40 @@ DISCOUNT_APPLICABILITY_FLAG = WaffleFlag(
DISCOUNT_APPLICABILITY_HOLDBACK = 'first_purchase_discount_holdback'
-def can_receive_discount(user, course): # pylint: disable=unused-argument
+def get_discount_expiration_date(user, course):
+ """
+ Returns the date when the discount expires for the user.
+ Returns none if the user is not enrolled.
+ """
+ course_enrollment = CourseEnrollment.objects.filter(
+ user=user,
+ course=course.id,
+ mode__in=CourseMode.UPSELL_TO_VERIFIED_MODES
+ )
+ if len(course_enrollment) != 1:
+ return None
+
+ enrollment = course_enrollment.first()
+ try:
+ # Content availability date is equivalent to max(enrollment date, course start date)
+ # for most people. Using the schedule date will provide flexibility to deal with
+ # more complex business rules in the future.
+ content_availability_date = enrollment.schedule.start
+ # We have anecdotally observed a case where the schedule.start was
+ # equal to the course start, but should have been equal to the enrollment start
+ # https://openedx.atlassian.net/browse/PROD-58
+ # This section is meant to address that case
+ if enrollment.created and course.start:
+ if (content_availability_date.date() == course.start.date() and
+ course.start < enrollment.created < timezone.now()):
+ content_availability_date = enrollment.created
+ except CourseEnrollment.schedule.RelatedObjectDoesNotExist:
+ content_availability_date = max(enrollment.created, course.start)
+
+ return content_availability_date + timedelta(weeks=1)
+
+
+def can_receive_discount(user, course, discount_expiration_date=None):
"""
Check all the business logic about whether this combination of user and course
can receive a discount.
@@ -55,6 +89,16 @@ def can_receive_discount(user, course): # pylint: disable=unused-argument
# TODO: Add additional conditions to return False here
+ # Check if discount has expired
+ if not discount_expiration_date:
+ discount_expiration_date = get_discount_expiration_date(user, course)
+
+ if discount_expiration_date is None:
+ return False
+
+ if discount_expiration_date < timezone.now():
+ return False
+
# Course end date needs to be in the future
if course.has_ended():
return False
diff --git a/openedx/features/discounts/tests/test_applicability.py b/openedx/features/discounts/tests/test_applicability.py
index 1876747231..258e2a1548 100644
--- a/openedx/features/discounts/tests/test_applicability.py
+++ b/openedx/features/discounts/tests/test_applicability.py
@@ -53,6 +53,12 @@ class TestApplicability(ModuleStoreTestCase):
"""
Ensure first purchase offer banner only displays for courses with a non-expired verified mode
"""
+ CourseEnrollmentFactory(
+ is_active=True,
+ course_id=self.course.id,
+ user=self.user
+ )
+
applicability = can_receive_discount(user=self.user, course=self.course)
self.assertEqual(applicability, True)
@@ -86,6 +92,12 @@ class TestApplicability(ModuleStoreTestCase):
"""
Ensure that only users who have not already purchased courses receive the discount.
"""
+ CourseEnrollmentFactory(
+ is_active=True,
+ course_id=self.course.id,
+ user=self.user
+ )
+
for mode in existing_enrollments:
CourseEnrollmentFactory.create(mode=mode, user=self.user)
@@ -102,6 +114,12 @@ class TestApplicability(ModuleStoreTestCase):
"""
Ensure that only users who have not already purchased courses receive the discount.
"""
+ CourseEnrollmentFactory(
+ is_active=True,
+ course_id=self.course.id,
+ user=self.user
+ )
+
if entitlement_mode is not None:
CourseEntitlementFactory.create(mode=entitlement_mode, user=self.user)
diff --git a/openedx/features/discounts/utils.py b/openedx/features/discounts/utils.py
index 8c490bea7f..bdd6bda86d 100644
--- a/openedx/features/discounts/utils.py
+++ b/openedx/features/discounts/utils.py
@@ -9,7 +9,7 @@ from openedx.core.djangolib.markup import HTML
from .applicability import can_receive_discount, discount_percentage
-def format_strikeout_price(user, course, base_price=None):
+def format_strikeout_price(user, course, base_price=None, check_for_discount=True):
"""
Return a formatted price, including a struck-out original price if a discount applies, and also
whether a discount was applied, as the tuple (formatted_price, has_discount).
@@ -19,7 +19,7 @@ def format_strikeout_price(user, course, base_price=None):
original_price = format_course_price(base_price)
- if can_receive_discount(user, course):
+ if not check_for_discount or can_receive_discount(user, course):
discount_price = base_price * ((100.0 - discount_percentage()) / 100)
if discount_price == int(discount_price):
discount_price = format_course_price("{:0.0f}".format(discount_price))