Merge pull request #20674 from edx/REVEM-354

add discount banner to track selection and discount price to upgrade button
This commit is contained in:
Matthew Piatetsky
2019-06-07 11:35:34 -04:00
committed by GitHub
12 changed files with 169 additions and 123 deletions

1
.github/CODEOWNERS vendored
View File

@@ -38,3 +38,4 @@ lms/djangoapps/experiments/ @edx/rev-team
lms/djangoapps/learner_dashboard/ @edx/platform-discovery
openedx/features/content_type_gating/ @edx/rev-team
openedx/features/course_duration_limits/ @edx/rev-team
openedx/features/discounts/ @edx/rev-team

View File

@@ -23,6 +23,8 @@ from lms.djangoapps.commerce.tests import test_utils as ecomm_test_utils
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.embargo.test_utils import restrict_course
from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
from openedx.core.djangoapps.waffle_utils.testutils import override_waffle_flag
from openedx.features.course_experience import FIRST_PURCHASE_OFFER_BANNER_DISPLAY
from student.models import CourseEnrollment
from student.tests.factories import CourseEnrollmentFactory, UserFactory
from util.testing import UrlResetMixin
@@ -397,6 +399,36 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
self.assertEquals(course_mode, expected_mode)
@patch('openedx.features.course_experience.utils.can_receive_discount')
@patch('openedx.features.course_experience.utils.discount_percentage')
@override_waffle_flag(FIRST_PURCHASE_OFFER_BANNER_DISPLAY, active=True)
def test_discount_on_track_selection(self, discount_percentage_mock, can_receive_discount_mock):
can_receive_discount_mock.return_value = True
discount_percentage_mock.return_value = 15
parameters = {
'mode_slug': 'verified',
'mode_display_name': 'Verified Certificate',
'min_price': 10
}
url = reverse('create_mode', args=[six.text_type(self.course.id)])
response = self.client.get(url, parameters)
response = self.client.get(
reverse('course_modes_choose', args=[six.text_type(self.course.id)]),
follow=False,
)
bannerText = u'''<div class="first-purchase-offer-banner"><span class="first-purchase-offer-banner-bold">
15% off your first upgrade.</span> Discount automatically applied.</div>'''
button = u'''<button type="submit" name="verified_mode">
<span>Pursue a Verified Certificate</span>
(<span class="upgrade-price-string">$8.50 USD</span>
<del> <span class="upgrade-price-string">$10 USD</span></del>)
</button>'''
self.assertContains(response, bannerText, html=True)
self.assertContains(response, button, html=True)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_multiple_mode_creation(self):
# Create an honor mode

View File

@@ -34,6 +34,8 @@ from openedx.core.djangoapps.catalog.utils import get_currency_data
from openedx.core.djangoapps.embargo import api as embargo_api
from openedx.features.content_type_gating.models import ContentTypeGatingConfig
from openedx.features.course_duration_limits.models import CourseDurationLimitConfig
from openedx.features.course_experience.utils import get_first_purchase_offer_banner_fragment
from openedx.features.discounts.applicability import discount_percentage
from student.models import CourseEnrollment
from util.db import outer_atomic
from xmodule.modulestore.django import modulestore
@@ -190,11 +192,22 @@ class ChooseModeView(View):
for x in verified_mode.suggested_prices.split(",")
if x.strip()
]
price_before_discount = verified_mode.min_price
context["currency"] = verified_mode.currency.upper()
context["min_price"] = verified_mode.min_price
context["min_price"] = price_before_discount
context["verified_name"] = verified_mode.name
context["verified_description"] = verified_mode.description
offer_banner_fragment = get_first_purchase_offer_banner_fragment(
request.user, course
)
if offer_banner_fragment:
context['offer_banner_fragment'] = offer_banner_fragment
discounted_price = "{:0.2f}".format(price_before_discount * ((100.0 - discount_percentage()) / 100))
context["min_price"] = discounted_price
context["price_before_discount"] = price_before_discount
if verified_mode.sku:
context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user)
context["ecommerce_payment_page"] = ecommerce_service.payment_page_url()

View File

@@ -49,7 +49,7 @@ class TrackSelectionPage(PageObject):
if mode == "verified":
# Check the first contribution option, then click the enroll button
self.q(css=".contribution-option > input").first.click()
self.q(css="input[name='verified_mode']").click()
self.q(css="button[name='verified_mode']").click()
return PaymentAndVerificationFlow(self.browser, self._course_id).wait_for_page()
elif mode == "audit":

View File

@@ -1556,10 +1556,15 @@
@include text-align(left);
}
.action-select input {
.action-select input, .action-select button {
@extend %btn-verify-primary;
}
.action-select button[name="verified_mode"] {
font-weight: 600;
padding: 10px 15px;
}
// extra register options/info
.title-expand {
@extend %t-copy-sub1;
@@ -2249,6 +2254,21 @@
margin-top: 20px;
}
}
// First purchase offer banner
.first-purchase-offer-banner {
background-color: #dee3f1;
font-size: 16px;
border-radius: 7px;
padding: 20px;
.first-purchase-offer-banner-bold {
font-weight: bold;
color: #23419f;
margin-right: 3px;
margin-left: 5px;
}
}
}
.reverify-blocked {

View File

@@ -0,0 +1,25 @@
<%page args="content_gating_enabled, course_duration_limit_enabled, min_price, price_before_discount" expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<%namespace name='static' file='../static_content.html'/>
<li class="action action-select">
<input type="hidden" name="contribution" value="${price_before_discount or min_price}" />
% if content_gating_enabled or course_duration_limit_enabled:
<button type="submit" name="verified_mode">
<span>${_('Pursue the Verified Track')}</span>
% else:
<button type="submit" name="verified_mode">
<span>${_('Pursue a Verified Certificate')}</span>
% endif
% if price_before_discount:
(<span class="upgrade-price-string">$${min_price} USD</span> <del> <span class="upgrade-price-string">${Text('${price} USD').format(price=price_before_discount)}</span></del>)
% else:
(<span class="upgrade-price-string">$${min_price} USD</span>)
% endif
</button>
</li>

View File

@@ -42,7 +42,7 @@ from openedx.core.djangolib.markup import HTML, Text
});
% if use_ecommerce_payment_flow:
$('input[name=verified_mode]').click(function(e){
$('button[name=verified_mode]').click(function(e){
e.preventDefault();
window.location.href = '${ecommerce_payment_page | n, js_escaped_string}?sku=' +
encodeURIComponent('${sku | n, js_escaped_string}');
@@ -77,6 +77,10 @@ from openedx.core.djangolib.markup import HTML, Text
</h3>
</header>
% if offer_banner_fragment:
${HTML(offer_banner_fragment.content)}
% endif
<form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form">
<%
b_tag_kwargs = {'b_start': HTML('<b>'), 'b_end': HTML('</b>')}
@@ -120,10 +124,7 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
<div class="copy-inline list-actions">
<ul class="list-actions">
<li class="action action-select">
<input type="hidden" name="contribution" value="${min_price}" />
<input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')} ($${min_price} USD)" />
</li>
<%include file='_upgrade_button.html' args='content_gating_enabled=content_gating_enabled, course_duration_limit_enabled=course_duration_limit_enabled, min_price=min_price, price_before_discount=price_before_discount' />
</ul>
</div>
</div>
@@ -164,18 +165,11 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
<div class="copy-inline list-actions">
<ul class="list-actions">
<li class="action action-select">
<input type="hidden" name="contribution" value="${min_price}" />
% if content_gating_enabled or course_duration_limit_enabled:
<input type="submit" name="verified_mode" value="${_('Pursue the Verified Track')} ($${min_price} USD)" />
% else:
<input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')} ($${min_price} USD)" />
% endif
</li>
<%include file='_upgrade_button.html' args='content_gating_enabled=content_gating_enabled, course_duration_limit_enabled=course_duration_limit_enabled, min_price=min_price, price_before_discount=price_before_discount' />
</ul>
</div>
</div>
</p>
</p>
</div>
% endif
</div>

View File

@@ -1,2 +1,3 @@
<div id="currency_data" value='{"CAN": {"rate": 2.2, "code": "CAD", "symbol": "$"}}'></div>
<input type="submit" name="verified_mode" value="Pursue a Verified Certificate ($100 USD)">
<button type="submit" class="no-discount" name="verified_mode"><span>Pursue a Verified Certificate</span>(<span class="upgrade-price-string">$100 USD</span>)</button>
<button type="submit" class="discount" name="verified_mode"><span>Pursue a Verified Certificate</span>(<span class="upgrade-price-string">$90 USD</span> <del><span class="upgrade-price-string">$100 USD</span></del>)</button>

View File

@@ -4,17 +4,28 @@ import $ from 'jquery'; // eslint-disable-line import/extensions
export class Currency { // eslint-disable-line import/prefer-default-export
setPrice() {
editText(price) {
const l10nCookie = this.countryL10nData;
const lmsregex = /(\$)(\d*)( USD)/g;
const price = $('input[name="verified_mode"]').filter(':visible')[0];
const regexMatch = lmsregex.exec(price.value);
const dollars = parseFloat(regexMatch[2]);
const converted = dollars * l10nCookie.rate;
const string = `${l10nCookie.symbol}${Math.round(converted)} ${l10nCookie.code}`;
// Use regex to change displayed price on track selection
// based on edx-price-l10n cookie currency_data
price.value = price.value.replace(regexMatch[0], string);
const lmsregex = /(\$)([\d|.]*)( USD)/g;
const priceText = price.text();
const regexMatch = lmsregex.exec(priceText);
if (regexMatch) {
const currentPrice = regexMatch[2];
const dollars = parseFloat(currentPrice);
const newPrice = dollars * l10nCookie.rate;
const newPriceString = `${l10nCookie.symbol}${Math.round(newPrice)} ${l10nCookie.code}`;
// Change displayed price based on edx-price-l10n cookie currency_data
price.text(newPriceString);
}
}
setPrice() {
$('.upgrade-price-string').each((i, price) => {
// When the button includes two prices (discounted and previous)
// we call the method twice, since it modifies one price at a time.
// Could also be used to modify all prices on any page
this.editText($(price));
});
}
getCountry() {

View File

@@ -36,12 +36,17 @@ describe('Currency factory', () => {
it('when location is the default (US)', () => {
$.cookie('edx-price-l10n', '{"rate":1,"code":"USD","symbol":"$","countryCode":"US"}', { path: '/' });
currency = new Currency();
expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($100 USD)');
expect($('[name="verified_mode"].no-discount').filter(':visible').text()).toEqual('Pursue a Verified Certificate($100 USD)');
});
it('when cookie is set to a different country', () => {
$.cookie('edx-price-l10n', '{"rate":2.2,"code":"CAD","symbol":"$","countryCode":"CAN"}', { expires: 1 });
currency = new Currency();
expect($('input[name="verified_mode"]').filter(':visible')[0].value).toEqual('Pursue a Verified Certificate ($220 CAD)');
expect($('[name="verified_mode"].no-discount').filter(':visible').text()).toEqual('Pursue a Verified Certificate($220 CAD)');
});
it('when cookie is set to a different country with a discount', () => {
$.cookie('edx-price-l10n', '{"rate":2.2,"code":"CAD","symbol":"$","countryCode":"CAN"}', { expires: 1 });
currency = new Currency();
expect($('[name="verified_mode"].discount').filter(':visible').text()).toEqual('Pursue a Verified Certificate($198 CAD $220 CAD)');
});
});
});

View File

@@ -0,0 +1,25 @@
<%page args="content_gating_enabled, course_duration_limit_enabled, min_price, price_before_discount" expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<%namespace name='static' file='../static_content.html'/>
<li class="action action-select">
<input type="hidden" name="contribution" value="${price_before_discount or min_price}" />
% if content_gating_enabled or course_duration_limit_enabled:
<button type="submit" name="verified_mode">
<span>${_('Pursue the Verified Track')}</span>
% else:
<button type="submit" name="verified_mode">
<span>${_('Pursue a Verified Certificate')}</span>
% endif
% if price_before_discount:
(<span class="upgrade-price-string">$${min_price} USD</span> <del> <span class="upgrade-price-string">${Text('${price} USD').format(price=price_before_discount)}</span></del>)
% else:
(<span class="upgrade-price-string">$${min_price} USD</span>)
% endif
</button>
</li>

View File

@@ -45,16 +45,11 @@ from openedx.core.djangolib.markup import HTML, Text
});
% if use_ecommerce_payment_flow:
$('input[name=verified_mode]').click(function(e){
$('button[name=verified_mode]').click(function(e){
e.preventDefault();
window.location.href = '${ecommerce_payment_page | n, js_escaped_string}?sku=' +
encodeURIComponent('${sku | n, js_escaped_string}');
});
$('.v2 button[name=verified_mode]').click(function(e){
e.preventDefault();
window.location.href = 'https://ecommerce.edx.org/coupons/redeem/?code=EDXTSV35&sku=' +
encodeURIComponent('${sku | n, js_escaped_string}');
});
% endif
});
</script>
@@ -83,54 +78,27 @@ from openedx.core.djangolib.markup import HTML, Text
%endif
<div id="currency_data" value="${currency_data}"></div>
<div class="container">
<section class="wrapper">
<div class="wrapper-register-choose wrapper-content-main">
<article class="register-choose content-main">
<header class="page-header content-main">
<h3 class="title v1">
<h3 class="title">
${title_content}
</h3>
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<h3 class="title v2 hidden">
Next, Select Your Learning Path
</h3>
</header>
% if offer_banner_fragment:
${HTML(offer_banner_fragment.content)}
% endif
<form class="form-register-choose" method="post" name="enrollment_mode_form" id="enrollment_mode_form">
<%
b_tag_kwargs = {'b_start': HTML('<b>'), 'b_end': HTML('</b>')}
%>
% if "verified" in modes:
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<div class="register-choice register-choice-certificate v2 hidden">
<h4 class="title">Pursue a Verified Certificate</h4>
<div class="wrapper-copy-inline">
<div class="wrapper-copy-inline">
<div class="wrapper-copy">
Get premium level support throughout the course and commit to completion up front.
</div>
<img src="/static/images/cert-verified-thumb.png" class="visual-reference img-certificate" alt="Visual representation of a Certificate"/>
</div>
</div>
<div class="copy-inline">
<ul class="list-actions">
<li class="action action-select">
<input type="hidden" name="contribution" value="${min_price}" />
<!-- The class verified_mode should be added to this selector if the experiment is implemented-->
<div class="upgradev1">
<input type="submit" name="verified_mode" value="Upgrade to a Certificate ($${min_price} USD)" />
</div>
<div class="upgradev2 hidden">
<button type="submit" name="verified_mode">Upgrade to a Certificate (<del>$${min_price} USD</del>)</button>
<br>
<div class="savings-message">Save 5% if you upgrade now! ($${int(min_price * .95)} USD)</div>
</div>
</li>
</ul>
</div>
</div>
<div class="register-choice register-choice-certificate v1">
<div class="register-choice register-choice-certificate">
<div class="wrapper-copy">
<span class="deco-ribbon"></span>
% if has_credit_upsell:
@@ -169,10 +137,7 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
<div class="copy-inline list-actions">
<ul class="list-actions">
<li class="action action-select">
<input type="hidden" name="contribution" value="${min_price}" />
<input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')} ($${min_price} USD)" />
</li>
<%include file='_upgrade_button.html' args='content_gating_enabled=content_gating_enabled, course_duration_limit_enabled=course_duration_limit_enabled, min_price=min_price, price_before_discount=price_before_discount' />
</ul>
</div>
</div>
@@ -215,14 +180,7 @@ from openedx.core.djangolib.markup import HTML, Text
</div>
<div class="copy-inline list-actions">
<ul class="list-actions">
<li class="action action-select">
<input type="hidden" name="contribution" value="${min_price}" />
% if content_gating_enabled or course_duration_limit_enabled:
<input type="submit" name="verified_mode" value="${_('Pursue the Verified Track')} ($${min_price} USD)" />
% else:
<input type="submit" name="verified_mode" value="${_('Pursue a Verified Certificate')} ($${min_price} USD)" />
% endif
</li>
<%include file='_upgrade_button.html' args='content_gating_enabled=content_gating_enabled, course_duration_limit_enabled=course_duration_limit_enabled, min_price=min_price, price_before_discount=price_before_discount' />
</ul>
</div>
</div>
@@ -254,50 +212,11 @@ from openedx.core.djangolib.markup import HTML, Text
</ul>
</div>
% elif "audit" in modes:
<span class="deco-divider v1">
<span class="copy">${_("or")}</span>
</span>
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<span class="deco-divider v2 hidden">
<span class="deco-divider">
<span class="copy">${_("or")}</span>
</span>
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<div class="register-choice register-choice-continue v2 hidden">
<h4 class="title">
I Don't Want to Upgrade or Donate Today
</h4>
<div class="wrapper-copy-inline">
<div class="wrapper-copy">
If you do not want to add a certificate or donate to edX's mission today, you can skip this step for now and continue to the course.
</div>
<img src="/static/images/edx-home-graphic.png" class="visual-reference img-donate" alt="Visual of two hands forming a heart shape" >
</div>
<div class="copy-inline">
<ul class="list-actions">
<li class="action action-select">
<a class="continue-link" href="/dashboard">Continue to Course</a>
</li>
</ul>
</div>
</div>
<!-- This div was added as part of the LEARNER-1726 experiment. The v2 class should be removed if the experiment is implemented-->
<div class="register-choice register-choice-v2-donate register-choice-view v2 hidden">
<h4 class="title">Donate to Support our Non-Profit Mission</h4>
<div class="wrapper-copy-inline">
<div class="wrapper-copy">
Even if you are not interested in pursuing a Verified Certificate, a donation helps edX continue to work towards its non-profit mission of making the world's best education more accessible to learners everywhere.
</div>
<div class="copy-inline">
<ul class="list-actions">
<a class="donation-link" href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=AG9VK2LC29L5Y">Donate and Continue to Course</a>
</ul>
</div>
</div>
</div>
<div class="register-choice register-choice-audit v1">
<div class="register-choice register-choice-audit">
<div class="wrapper-copy">
<span class="deco-ribbon"></span>
<h4 class="title">${_("Audit This Course (No Certificate)")}</h4>