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..d267da32de 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,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'''
''' + 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/_upgrade_button.html b/lms/templates/course_modes/_upgrade_button.html new file mode 100644 index 0000000000..c4482ab82c --- /dev/null +++ b/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'/> + +