diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 7fad3f16c5..7f7e3a7b76 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -130,7 +130,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest # Configure whether we're upgrading or not url = reverse('course_modes_choose', args=[unicode(prof_course.id)]) response = self.client.get(url) - self.assertRedirects(response, 'http://testserver/test_basket/?sku=TEST', fetch_redirect_response=False) + self.assertRedirects(response, 'http://testserver/basket/add/?sku=TEST', fetch_redirect_response=False) ecomm_test_utils.update_commerce_config(enabled=False) def _generate_enterprise_learner_context(self, enable_audit_enrollment=False): diff --git a/lms/djangoapps/commerce/models.py b/lms/djangoapps/commerce/models.py index 2363f137a0..cedb50799c 100644 --- a/lms/djangoapps/commerce/models.py +++ b/lms/djangoapps/commerce/models.py @@ -15,6 +15,7 @@ class CommerceConfiguration(ConfigurationModel): API_NAME = 'commerce' CACHE_KEY = 'commerce.api.data' DEFAULT_RECEIPT_PAGE_URL = '/checkout/receipt/?order_number=' + MULTIPLE_ITEMS_BASKET_PAGE_URL = '/basket/add/' checkout_on_ecommerce_service = models.BooleanField( default=False, diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index 201923ee45..145332fa80 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -97,7 +97,9 @@ class EcommerceServiceTests(TestCase): def test_get_checkout_page_url(self, skus): """ Verify the checkout page URL is properly constructed and returned. """ url = EcommerceService().get_checkout_page_url(*skus) - expected_url = '{root}/test_basket/?{skus}'.format( + config = CommerceConfiguration.current() + expected_url = '{root}{basket_url}?{skus}'.format( + basket_url=config.MULTIPLE_ITEMS_BASKET_PAGE_URL, root=settings.ECOMMERCE_PUBLIC_URL_ROOT, skus=urlencode({'sku': skus}, doseq=True), ) diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 644492e7bf..7de0e6d6f1 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -87,9 +87,9 @@ class EcommerceService(object): Absolute path to the ecommerce checkout page showing basket that contains specified products. Example: - http://localhost:8002/basket/single_item/?sku=5H3HG5&sku=57FHHD + http://localhost:8002/basket/add/?sku=5H3HG5&sku=57FHHD """ return '{checkout_page_path}?{skus}'.format( - checkout_page_path=self.get_absolute_ecommerce_url(self.config.single_course_checkout_page), + checkout_page_path=self.get_absolute_ecommerce_url(self.config.MULTIPLE_ITEMS_BASKET_PAGE_URL), skus=urlencode({'sku': skus}, doseq=True), ) diff --git a/lms/djangoapps/courseware/tests/test_date_summary.py b/lms/djangoapps/courseware/tests/test_date_summary.py index 3c4cf314f2..3ebc7f1239 100644 --- a/lms/djangoapps/courseware/tests/test_date_summary.py +++ b/lms/djangoapps/courseware/tests/test_date_summary.py @@ -313,14 +313,10 @@ class CourseDateSummaryTest(SharedModuleStoreTestCase): def test_ecommerce_checkout_redirect(self): """Verify the block link redirects to ecommerce checkout if it's enabled.""" sku = 'TESTSKU' - checkout_page = '/test_basket/' - CommerceConfiguration.objects.create( - checkout_on_ecommerce_service=True, - single_course_checkout_page=checkout_page - ) + configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True) self.setup_course_and_user(sku=sku) block = VerifiedUpgradeDeadlineDate(self.course, self.user) - self.assertEqual(block.link, '{}?sku={}'.format(checkout_page, sku)) + self.assertEqual(block.link, '{}?sku={}'.format(configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL, sku)) ## VerificationDeadlineDate def test_no_verification_deadline(self): diff --git a/lms/djangoapps/courseware/tests/test_views.py b/lms/djangoapps/courseware/tests/test_views.py index bf04cd62e8..c9a23c8f01 100644 --- a/lms/djangoapps/courseware/tests/test_views.py +++ b/lms/djangoapps/courseware/tests/test_views.py @@ -469,12 +469,8 @@ class ViewsTestCase(ModuleStoreTestCase): _id(bool): Tell the method to either expect an id in the href or not. """ - checkout_page = '/test_basket/' sku = 'TEST123' - CommerceConfiguration.objects.create( - checkout_on_ecommerce_service=True, - single_course_checkout_page=checkout_page - ) + configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True) course = CourseFactory.create() CourseModeFactory(mode_slug=CourseMode.PROFESSIONAL, course_id=course.id, sku=sku, min_price=1) @@ -486,7 +482,10 @@ class ViewsTestCase(ModuleStoreTestCase): # Construct the link according the following scenarios and verify its presence in the response: # (1) shopping cart is enabled and the user is not logged in # (2) shopping cart is enabled and the user is logged in - href = ''.format(uri_stem=checkout_page, sku=sku) + href = ''.format( + uri_stem=configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL, + sku=sku, + ) # Generate the course about page content response = self.client.get(reverse('about_course', args=[unicode(course.id)])) diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 0a2253a11e..bb7f67ad61 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -131,7 +131,6 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): ) def test_start_flow_with_ecommerce(self): """Verify user gets redirected to ecommerce checkout when ecommerce checkout is enabled.""" - checkout_page = '/test_basket/' sku = 'TESTSKU' # When passing a SKU ecommerce api gets called. httpretty.register_uri( @@ -140,11 +139,10 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase, XssTestMixin): body=json.dumps(['foo', 'bar']), content_type="application/json", ) + configuration = CommerceConfiguration.objects.create(checkout_on_ecommerce_service=True) + checkout_page = configuration.MULTIPLE_ITEMS_BASKET_PAGE_URL httpretty.register_uri(httpretty.GET, "{}{}".format(TEST_PUBLIC_URL_ROOT, checkout_page)) - CommerceConfiguration.objects.create( - checkout_on_ecommerce_service=True, - single_course_checkout_page=checkout_page - ) + course = self._create_course('verified', sku=sku) self._enroll(course.id) response = self._get_page('verify_student_start_flow', course.id, expected_status_code=302) diff --git a/lms/static/sass/views/_program-marketing-page.scss b/lms/static/sass/views/_program-marketing-page.scss index e0d57ff8eb..2e25c00fa0 100644 --- a/lms/static/sass/views/_program-marketing-page.scss +++ b/lms/static/sass/views/_program-marketing-page.scss @@ -1085,6 +1085,26 @@ } } } + + .description { + display: block; + float: left; + } + + .price { + .green-highlight { + font-weight: 700; + color: palette(success, text); + } + + .original-price { + text-decoration: line-through; + } + + .savings { + display: block; + } + } } } diff --git a/lms/templates/courseware/program_marketing.html b/lms/templates/courseware/program_marketing.html index b9eb161ed0..d1672efa9a 100644 --- a/lms/templates/courseware/program_marketing.html +++ b/lms/templates/courseware/program_marketing.html @@ -69,7 +69,7 @@ description_max_length = 250 % else: @@ -99,7 +99,7 @@ description_max_length = 250
diff --git a/openedx/core/djangoapps/programs/tests/test_utils.py b/openedx/core/djangoapps/programs/tests/test_utils.py index 59dfd28cb5..d4c968e255 100644 --- a/openedx/core/djangoapps/programs/tests/test_utils.py +++ b/openedx/core/djangoapps/programs/tests/test_utils.py @@ -872,6 +872,19 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): self.program['applicable_seat_types'] = [seat['type']] return seat + def _update_discount_data(self, mock_discount_data): + """ + Helper method that updates mocked discount data with + - a flag indicating whether the program price is discounted + - the amount of the discount (0 in case there's no discount) + """ + program_discounted_price = mock_discount_data['total_incl_tax'] + program_full_price = mock_discount_data['total_incl_tax_excl_discounts'] + mock_discount_data.update({ + 'is_discounted': program_discounted_price < program_full_price, + 'discount_value': program_full_price - program_discounted_price + }) + def test_instructors(self): data = ProgramMarketingDataExtender(self.program, self.user).extend() @@ -887,10 +900,13 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): self.assertEqual(data['avg_price_per_course'], program_full_price / self.number_of_courses) def test_course_pricing_when_all_course_runs_have_no_seats(self): - course = ModuleStoreCourseFactory() - course = self.update_course(course, self.user.id) - course_run = CourseRunFactory(key=unicode(course.id), seats=[]) - program = ProgramFactory(courses=[CourseFactory(course_runs=[course_run])]) + # Create three seatless course runs and add them to the program + course_runs = [] + for __ in range(3): + course = ModuleStoreCourseFactory() + course = self.update_course(course, self.user.id) + course_runs.append(CourseRunFactory(key=unicode(course.id), seats=[])) + program = ProgramFactory(courses=[CourseFactory(course_runs=course_runs)]) data = ProgramMarketingDataExtender(program, self.user).extend() @@ -988,7 +1004,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): self._prepare_program_for_discounted_price_calculation_endpoint() mock_discount_data = { 'total_incl_tax_excl_discounts': 200.0, - 'currency': "USD", + 'currency': 'USD', 'total_incl_tax': 50.0 } httpretty.register_uri( @@ -999,6 +1015,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ) data = ProgramMarketingDataExtender(self.program, self.user).extend() + self._update_discount_data(mock_discount_data) self.assertEqual( data['skus'], @@ -1015,7 +1032,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): self._prepare_program_for_discounted_price_calculation_endpoint() mock_discount_data = { 'total_incl_tax_excl_discounts': 200.0, - 'currency': "USD", + 'currency': 'USD', 'total_incl_tax': 50.0 } httpretty.register_uri( @@ -1026,6 +1043,7 @@ class TestProgramMarketingDataExtender(ModuleStoreTestCase): ) data = ProgramMarketingDataExtender(self.program, AnonymousUserFactory()).extend() + self._update_discount_data(mock_discount_data) self.assertEqual( data['skus'], diff --git a/openedx/core/djangoapps/programs/utils.py b/openedx/core/djangoapps/programs/utils.py index a5a1fc6cf1..205ce16aac 100644 --- a/openedx/core/djangoapps/programs/utils.py +++ b/openedx/core/djangoapps/programs/utils.py @@ -605,7 +605,7 @@ class ProgramMarketingDataExtender(ProgramDataExtender): published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs']) if len(published_course_runs) == 1: for seat in published_course_runs[0]['seats']: - if seat['type'] in applicable_seat_types: + if seat['type'] in applicable_seat_types and seat['sku']: skus.append(seat['sku']) else: # If a course in the program has more than 1 published course run @@ -626,6 +626,11 @@ class ProgramMarketingDataExtender(ProgramDataExtender): # Make an API call to calculate the discounted price discount_data = api.baskets.calculate.get(sku=skus) + program_discounted_price = discount_data['total_incl_tax'] + program_full_price = discount_data['total_incl_tax_excl_discounts'] + discount_data['is_discounted'] = program_discounted_price < program_full_price + discount_data['discount_value'] = program_full_price - program_discounted_price + self.data.update({ 'discount_data': discount_data, 'full_program_price': discount_data['total_incl_tax']