From ad77392f8d357c927308c6e8d8bb9a0b46d59b7b Mon Sep 17 00:00:00 2001 From: Matthew Piatetsky Date: Wed, 22 May 2019 16:45:50 -0400 Subject: [PATCH 1/2] add discount banner to track selection --- .github/CODEOWNERS | 1 + .../course_modes/tests/test_views.py | 31 +++++ common/djangoapps/course_modes/views.py | 15 ++- .../acceptance/pages/lms/track_selection.py | 2 +- lms/static/sass/views/_verification.scss | 22 +++- lms/templates/course_modes/choose.html | 30 +++-- .../lms/templates/course_modes/choose.html | 113 +++++------------- 7 files changed, 118 insertions(+), 96 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fb2367e6b1..693a3e688b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index d836b4ea80..78a4ea34fe 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -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,35 @@ 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'''
+ 15% off your first upgrade. Discount automatically applied.
''' + button = u'''''' + 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 diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 9eb116bf27..25f324c0d7 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -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() diff --git a/common/test/acceptance/pages/lms/track_selection.py b/common/test/acceptance/pages/lms/track_selection.py index 4f519aa1e2..11d12d34be 100644 --- a/common/test/acceptance/pages/lms/track_selection.py +++ b/common/test/acceptance/pages/lms/track_selection.py @@ -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": diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss index ed2fdbc320..4e06aba456 100644 --- a/lms/static/sass/views/_verification.scss +++ b/lms/static/sass/views/_verification.scss @@ -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 { diff --git a/lms/templates/course_modes/choose.html b/lms/templates/course_modes/choose.html index 7859ab228b..80dc286566 100644 --- a/lms/templates/course_modes/choose.html +++ b/lms/templates/course_modes/choose.html @@ -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 + % if offer_banner_fragment: + ${HTML(offer_banner_fragment.content)} + % endif +
<% b_tag_kwargs = {'b_start': HTML(''), 'b_end': HTML('')} @@ -121,8 +125,12 @@ from openedx.core.djangolib.markup import HTML, Text
  • - - + +
@@ -165,17 +173,25 @@ from openedx.core.djangolib.markup import HTML, Text
  • - + % if content_gating_enabled or course_duration_limit_enabled: - + % else: - + % endif
-

+

% endif diff --git a/themes/edx.org/lms/templates/course_modes/choose.html b/themes/edx.org/lms/templates/course_modes/choose.html index 79ed10a561..557328a2e4 100644 --- a/themes/edx.org/lms/templates/course_modes/choose.html +++ b/themes/edx.org/lms/templates/course_modes/choose.html @@ -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 }); @@ -83,54 +78,27 @@ from openedx.core.djangolib.markup import HTML, Text %endif
+
+ % if offer_banner_fragment: + ${HTML(offer_banner_fragment.content)} + % endif + <% b_tag_kwargs = {'b_start': HTML(''), 'b_end': HTML('')} %> % if "verified" in modes: - - -
+
% if has_credit_upsell: @@ -170,8 +138,12 @@ from openedx.core.djangolib.markup import HTML, Text
  • - - + +
@@ -216,11 +188,19 @@ from openedx.core.djangolib.markup import HTML, Text
  • - + % if content_gating_enabled or course_duration_limit_enabled: - + % else: - + % endif
@@ -254,50 +234,11 @@ from openedx.core.djangolib.markup import HTML, Text
% elif "audit" in modes: - - ${_("or")} - - -
diff --git a/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html b/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html index 9a65d28651..2bbec514f4 100644 --- a/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html +++ b/openedx/features/course_experience/static/course_experience/fixtures/course-currency-fragment.html @@ -1,2 +1,3 @@
- + + diff --git a/openedx/features/course_experience/static/course_experience/js/currency.js b/openedx/features/course_experience/static/course_experience/js/currency.js index be5c3c63a0..d9fa2a80ef 100644 --- a/openedx/features/course_experience/static/course_experience/js/currency.js +++ b/openedx/features/course_experience/static/course_experience/js/currency.js @@ -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() { diff --git a/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js b/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js index f6f57de098..45e59ee254 100644 --- a/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js +++ b/openedx/features/course_experience/static/course_experience/js/spec/Currency_spec.js @@ -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)'); }); }); }); diff --git a/themes/edx.org/lms/templates/course_modes/_upgrade_button.html b/themes/edx.org/lms/templates/course_modes/_upgrade_button.html new file mode 100644 index 0000000000..c4482ab82c --- /dev/null +++ b/themes/edx.org/lms/templates/course_modes/_upgrade_button.html @@ -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'/> + +
  • + + % if content_gating_enabled or course_duration_limit_enabled: + +
  • \ No newline at end of file diff --git a/themes/edx.org/lms/templates/course_modes/choose.html b/themes/edx.org/lms/templates/course_modes/choose.html index 557328a2e4..ea35afc96e 100644 --- a/themes/edx.org/lms/templates/course_modes/choose.html +++ b/themes/edx.org/lms/templates/course_modes/choose.html @@ -137,14 +137,7 @@ from openedx.core.djangolib.markup import HTML, Text
      -
    • - - -
    • + <%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' />
    @@ -187,22 +180,7 @@ from openedx.core.djangolib.markup import HTML, Text
      -
    • - - % if content_gating_enabled or course_duration_limit_enabled: - - % else: - - % endif -
    • + <%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' />