diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 0aa9200217..b8c5161ddf 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -94,7 +94,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): url = reverse('course_modes_choose', args=[unicode(self.course.id)]) response = self.client.get(url) # Check whether we were correctly redirected - start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow = "?purchase_workflow=single" + start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow self.assertRedirects(response, start_flow_url) def test_no_id_redirect_otto(self): @@ -194,7 +195,8 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): # Since the only available track is professional ed, expect that # we're redirected immediately to the start of the payment flow. - start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow = "?purchase_workflow=single" + start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow self.assertRedirects(response, start_flow_url) # Now enroll in the course diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 2bf337e1b2..57695c9da2 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -88,12 +88,15 @@ class ChooseModeView(View): # If there are both modes, default to non-id-professional. has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode(modes) and not has_enrolled_professional: - redirect_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) + purchase_workflow = request.GET.get("purchase_workflow", "single") + verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) + redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL) - if professional_mode.sku: + if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.checkout_page_url(professional_mode.sku) - + if purchase_workflow == "bulk" and professional_mode.bulk_sku: + redirect_url = ecommerce_service.checkout_page_url(professional_mode.bulk_sku) return redirect(redirect_url) # If there isn't a verified mode available, then there's nothing diff --git a/common/djangoapps/student/helpers.py b/common/djangoapps/student/helpers.py index a8c4e500ff..8ea71514a9 100644 --- a/common/djangoapps/student/helpers.py +++ b/common/djangoapps/student/helpers.py @@ -200,7 +200,7 @@ def auth_pipeline_urls(auth_entry, redirect_url=None): # Query string parameters that can be passed to the "finish_auth" view to manage # things like auto-enrollment. -POST_AUTH_PARAMS = ('course_id', 'enrollment_action', 'course_mode', 'email_opt_in') +POST_AUTH_PARAMS = ('course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow') def get_next_url_for_login_page(request): diff --git a/common/lib/xmodule/xmodule/contentstore/content.py b/common/lib/xmodule/xmodule/contentstore/content.py index b6f11e0979..8b5ae95ea0 100644 --- a/common/lib/xmodule/xmodule/contentstore/content.py +++ b/common/lib/xmodule/xmodule/contentstore/content.py @@ -44,13 +44,17 @@ class StaticContent(object): return self.location.category == 'thumbnail' @staticmethod - def generate_thumbnail_name(original_name, dimensions=None): + def generate_thumbnail_name(original_name, dimensions=None, extension=None): """ - original_name: Name of the asset (typically its location.name) - dimensions: `None` or a tuple of (width, height) in pixels + - extension: `None` or desired filename extension of the thumbnail """ + if extension is None: + extension = XASSET_THUMBNAIL_TAIL_NAME + name_root, ext = os.path.splitext(original_name) - if not ext == XASSET_THUMBNAIL_TAIL_NAME: + if not ext == extension: name_root = name_root + ext.replace(u'.', u'-') if dimensions: @@ -59,7 +63,7 @@ class StaticContent(object): return u"{name_root}{extension}".format( name_root=name_root, - extension=XASSET_THUMBNAIL_TAIL_NAME, + extension=extension, ) @staticmethod @@ -330,9 +334,10 @@ class ContentStore(object): pixels. It defaults to None. """ thumbnail_content = None + is_svg = content.content_type == 'image/svg+xml' # use a naming convention to associate originals with the thumbnail thumbnail_name = StaticContent.generate_thumbnail_name( - content.location.name, dimensions=dimensions + content.location.name, dimensions=dimensions, extension='.svg' if is_svg else None ) thumbnail_file_location = StaticContent.compute_location( content.location.course_key, thumbnail_name, is_thumbnail=True @@ -340,28 +345,42 @@ class ContentStore(object): # if we're uploading an image, then let's generate a thumbnail so that we can # serve it up when needed without having to rescale on the fly - if content.content_type is not None and content.content_type.split('/')[0] == 'image': - try: + try: + if is_svg: + # for svg simply store the provided svg file, since vector graphics should be good enough + # for downscaling client-side + if tempfile_path is None: + thumbnail_file = StringIO.StringIO(content.data) + else: + with open(tempfile_path) as f: + thumbnail_file = StringIO.StringIO(f.read()) + thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name, + 'image/svg+xml', thumbnail_file) + self.save(thumbnail_content) + elif content.content_type is not None and content.content_type.split('/')[0] == 'image': # use PIL to do the thumbnail generation (http://www.pythonware.com/products/pil/) # My understanding is that PIL will maintain aspect ratios while restricting # the max-height/width to be whatever you pass in as 'size' # @todo: move the thumbnail size to a configuration setting?!? if tempfile_path is None: - im = Image.open(StringIO.StringIO(content.data)) + source = StringIO.StringIO(content.data) else: - im = Image.open(tempfile_path) + source = tempfile_path - # I've seen some exceptions from the PIL library when trying to save palletted - # PNG files to JPEG. Per the google-universe, they suggest converting to RGB first. - im = im.convert('RGB') - - if not dimensions: - dimensions = (128, 128) - - im.thumbnail(dimensions, Image.ANTIALIAS) + # We use the context manager here to avoid leaking the inner file descriptor + # of the Image object -- this way it gets closed after we're done with using it. thumbnail_file = StringIO.StringIO() - im.save(thumbnail_file, 'JPEG') - thumbnail_file.seek(0) + with Image.open(source) as image: + # I've seen some exceptions from the PIL library when trying to save palletted + # PNG files to JPEG. Per the google-universe, they suggest converting to RGB first. + thumbnail_image = image.convert('RGB') + + if not dimensions: + dimensions = (128, 128) + + thumbnail_image.thumbnail(dimensions, Image.ANTIALIAS) + thumbnail_image.save(thumbnail_file, 'JPEG') + thumbnail_file.seek(0) # store this thumbnail as any other piece of content thumbnail_content = StaticContent(thumbnail_file_location, thumbnail_name, @@ -369,9 +388,11 @@ class ContentStore(object): self.save(thumbnail_content) - except Exception, e: - # log and continue as thumbnails are generally considered as optional - logging.exception(u"Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(e))) + except Exception, exc: # pylint: disable=broad-except + # log and continue as thumbnails are generally considered as optional + logging.exception( + u"Failed to generate thumbnail for {0}. Exception: {1}".format(content.location, str(exc)) + ) return thumbnail_content, thumbnail_file_location diff --git a/common/lib/xmodule/xmodule/tests/test_content.py b/common/lib/xmodule/xmodule/tests/test_content.py index 4d151e20e3..834be425e6 100644 --- a/common/lib/xmodule/xmodule/tests/test_content.py +++ b/common/lib/xmodule/xmodule/tests/test_content.py @@ -3,6 +3,7 @@ import os import unittest import ddt +from mock import Mock, patch from path import Path as path from xmodule.contentstore.content import StaticContent, StaticContentStream @@ -58,6 +59,7 @@ class Content(object): def __init__(self, location, content_type): self.location = location self.content_type = content_type + self.data = None class FakeGridFsItem(object): @@ -84,6 +86,17 @@ class FakeGridFsItem(object): return chunk +class MockImage(Mock): + """ + This class pretends to be PIL.Image for purposes of thumbnails testing. + """ + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + @ddt.ddt class ContentTest(unittest.TestCase): def test_thumbnail_none(self): @@ -103,11 +116,43 @@ class ContentTest(unittest.TestCase): ) @ddt.unpack def test_generate_thumbnail_image(self, original_filename, thumbnail_filename): - contentStore = ContentStore() + content_store = ContentStore() content = Content(AssetLocation(u'mitX', u'800', u'ignore_run', u'asset', original_filename), None) - (thumbnail_content, thumbnail_file_location) = contentStore.generate_thumbnail(content) + (thumbnail_content, thumbnail_file_location) = content_store.generate_thumbnail(content) self.assertIsNone(thumbnail_content) - self.assertEqual(AssetLocation(u'mitX', u'800', u'ignore_run', u'thumbnail', thumbnail_filename), thumbnail_file_location) + self.assertEqual( + AssetLocation(u'mitX', u'800', u'ignore_run', u'thumbnail', thumbnail_filename), + thumbnail_file_location + ) + + @patch('xmodule.contentstore.content.Image') + def test_image_is_closed_when_generating_thumbnail(self, image_class_mock): + # We used to keep the Image's file descriptor open when generating a thumbnail. + # It should be closed after being used. + mock_image = MockImage() + image_class_mock.open.return_value = mock_image + + content_store = ContentStore() + content = Content(AssetLocation(u'mitX', u'800', u'ignore_run', u'asset', "monsters.jpg"), "image/jpeg") + content.data = 'mock data' + content_store.generate_thumbnail(content) + self.assertTrue(image_class_mock.open.called, "Image.open not called") + self.assertTrue(mock_image.close.called, "mock_image.close not called") + + def test_store_svg_as_thumbnail(self): + # We had a bug that caused generate_thumbnail to attempt to pass SVG to PIL to generate a thumbnail. + # SVG files should be stored in original form for thumbnail purposes. + content_store = ContentStore() + content_store.save = Mock() + thumbnail_filename = u'test.svg' + content = Content(AssetLocation(u'mitX', u'800', u'ignore_run', u'asset', u'test.svg'), 'image/svg+xml') + content.data = 'mock svg file' + (thumbnail_content, thumbnail_file_location) = content_store.generate_thumbnail(content) + self.assertEqual(thumbnail_content.data.read(), b'mock svg file') + self.assertEqual( + AssetLocation(u'mitX', u'800', u'ignore_run', u'thumbnail', thumbnail_filename), + thumbnail_file_location + ) def test_compute_location(self): # We had a bug that __ got converted into a single _. Make sure that substitution of INVALID_CHARS (like space) @@ -115,7 +160,10 @@ class ContentTest(unittest.TestCase): asset_location = StaticContent.compute_location( SlashSeparatedCourseKey('mitX', '400', 'ignore'), 'subs__1eo_jXvZnE .srt.sjson' ) - self.assertEqual(AssetLocation(u'mitX', u'400', u'ignore', u'asset', u'subs__1eo_jXvZnE_.srt.sjson', None), asset_location) + self.assertEqual( + AssetLocation(u'mitX', u'400', u'ignore', u'asset', u'subs__1eo_jXvZnE_.srt.sjson', None), + asset_location + ) def test_get_location_from_path(self): asset_location = StaticContent.get_location_from_path(u'/c4x/a/b/asset/images_course_image.jpg') diff --git a/common/lib/xmodule/xmodule/tests/test_video.py b/common/lib/xmodule/xmodule/tests/test_video.py index 9b5b49eaba..e11147a43b 100644 --- a/common/lib/xmodule/xmodule/tests/test_video.py +++ b/common/lib/xmodule/xmodule/tests/test_video.py @@ -758,6 +758,14 @@ class VideoExportTestCase(VideoDescriptorTestBase): with self.assertRaises(ValueError): self.descriptor.definition_to_xml(None) + def test_export_to_xml_unicode_characters(self): + """ + Test XML export handles the unicode characters. + """ + self.descriptor.display_name = '这是文' + xml = self.descriptor.definition_to_xml(None) + self.assertEqual(xml.get('display_name'), u'\u8fd9\u662f\u6587') + class VideoDescriptorIndexingTestCase(unittest.TestCase): """ diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 59a54ff785..31f9ecc4ad 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -38,7 +38,7 @@ from xmodule.exceptions import NotFoundError from xmodule.contentstore.content import StaticContent from .transcripts_utils import VideoTranscriptsMixin, Transcript, get_html5_ids -from .video_utils import create_youtube_string, get_poster, rewrite_video_url +from .video_utils import create_youtube_string, get_poster, rewrite_video_url, format_xml_exception_message from .bumper_utils import bumperize from .video_xfields import VideoFields from .video_handlers import VideoStudentViewHandlers, VideoStudioViewHandlers @@ -563,14 +563,16 @@ class VideoDescriptor(VideoFields, VideoTranscriptsMixin, VideoStudioViewHandler if key in self.fields and self.fields[key].is_set_on(self): try: xml.set(key, unicode(value)) - except ValueError as exception: - exception_message = "{message}, Block-location:{location}, Key:{key}, Value:{value}".format( - message=exception.message, - location=unicode(self.location), - key=key, - value=unicode(value) - ) - raise ValueError(exception_message) + except UnicodeDecodeError: + exception_message = format_xml_exception_message(self.location, key, value) + log.exception(exception_message) + # If exception is UnicodeDecodeError set value using unicode 'utf-8' scheme. + log.info("Setting xml value using 'utf-8' scheme.") + xml.set(key, unicode(value, 'utf-8')) + except ValueError: + exception_message = format_xml_exception_message(self.location, key, value) + log.exception(exception_message) + raise for source in self.html5_sources: ele = etree.Element('source') diff --git a/common/lib/xmodule/xmodule/video_module/video_utils.py b/common/lib/xmodule/xmodule/video_module/video_utils.py index 0749b54f2d..1c24303ada 100644 --- a/common/lib/xmodule/xmodule/video_module/video_utils.py +++ b/common/lib/xmodule/xmodule/video_module/video_utils.py @@ -98,6 +98,19 @@ def get_poster(video): return poster +def format_xml_exception_message(location, key, value): + """ + Generate exception message for VideoDescriptor class which will use for ValueError and UnicodeDecodeError + when setting xml attributes. + """ + exception_message = "Block-location:{location}, Key:{key}, Value:{value}".format( + location=unicode(location), + key=key, + value=value + ) + return exception_message + + def set_query_parameter(url, param_name, param_value): """ Given a URL, set or replace a query parameter and return the diff --git a/common/test/acceptance/tests/lms/test_learner_profile.py b/common/test/acceptance/tests/lms/test_learner_profile.py index 72ec3c6431..241bf6817c 100644 --- a/common/test/acceptance/tests/lms/test_learner_profile.py +++ b/common/test/acceptance/tests/lms/test_learner_profile.py @@ -4,8 +4,9 @@ End-to-end tests for Student's Profile Page. """ from contextlib import contextmanager -from datetime import datetime from bok_choy.web_app_test import WebAppTest +from datetime import datetime +from flaky import flaky from nose.plugins.attrib import attr from ...pages.common.logout import LogoutPage @@ -300,6 +301,7 @@ class OwnLearnerProfilePageTest(LearnerProfileTestMixin, WebAppTest): self.verify_profile_page_is_private(profile_page) self.verify_profile_page_view_event(username, user_id, visibility=self.PRIVACY_PRIVATE) + @flaky # TODO fix this, see TNL-4683 def test_fields_on_my_public_profile(self): """ Scenario: Verify that desired fields are shown when looking at her own public profile. diff --git a/common/test/acceptance/tests/lms/test_programs.py b/common/test/acceptance/tests/lms/test_programs.py index 5a29d1b9b9..4695b583fc 100644 --- a/common/test/acceptance/tests/lms/test_programs.py +++ b/common/test/acceptance/tests/lms/test_programs.py @@ -1,6 +1,4 @@ """Acceptance tests for LMS-hosted Programs pages""" -from unittest import skip - from nose.plugins.attrib import attr from ...fixtures.programs import ProgramsFixture, ProgramsConfigMixin @@ -139,7 +137,6 @@ class ProgramListingPageA11yTest(ProgramPageBase): @attr('a11y') -@skip('The tested page is currently disabled. This test will be re-enabled once a11y failures are resolved.') class ProgramDetailsPageA11yTest(ProgramPageBase): """Test program details page accessibility.""" def setUp(self): diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 26014efc12..ca68e24190 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1240,8 +1240,15 @@ def get_students_features(request, course_id, csv=False): # pylint: disable=red available_features = instructor_analytics.basic.AVAILABLE_FEATURES - # Allow for microsites to be able to define additional columns (e.g. ) - query_features = microsite.get_value('student_profile_download_fields') + # Allow for microsites to be able to define additional columns. + # Note that adding additional columns has the potential to break + # the student profile report due to a character limit on the + # asynchronous job input which in this case is a JSON string + # containing the list of columns to include in the report. + # TODO: Refactor the student profile report code to remove the list of columns + # that should be included in the report from the asynchronous job input. + # We need to clone the list because we modify it below + query_features = list(microsite.get_value('student_profile_download_fields', [])) if not query_features: query_features = [ diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index 6591202d63..26352f0bf4 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -337,7 +337,7 @@ def submit_calculate_students_features_csv(request, course_key, features): """ task_type = 'profile_info_csv' task_class = calculate_students_features_csv - task_input = {'features': features} + task_input = features task_key = "" return submit_task(request, task_type, task_class, course_key, task_input, task_key) diff --git a/lms/djangoapps/instructor_task/tasks_helper.py b/lms/djangoapps/instructor_task/tasks_helper.py index c276d6de90..43f97ad9b9 100644 --- a/lms/djangoapps/instructor_task/tasks_helper.py +++ b/lms/djangoapps/instructor_task/tasks_helper.py @@ -996,7 +996,7 @@ def upload_students_csv(_xmodule_instance_args, _entry_id, course_id, task_input task_progress.update_task_state(extra_meta=current_step) # compute the student features table and format it - query_features = task_input.get('features') + query_features = task_input student_data = enrolled_students_features(course_id, query_features) header, rows = format_dictlist(student_data, query_features) diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py index 161874ba4b..1c51f667d0 100644 --- a/lms/djangoapps/learner_dashboard/tests/test_programs.py +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -261,7 +261,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): course_codes=[self.course_code] ) - def _mock_programs_api(self): + def _mock_programs_api(self, data, status=200): """Helper for mocking out Programs API URLs.""" self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.') @@ -269,9 +269,16 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): api_root=ProgramsApiConfig.current().internal_api_url.strip('/'), id=self.program_id ) - body = json.dumps(self.data) - httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json') + body = json.dumps(data) + + httpretty.register_uri( + httpretty.GET, + url, + body=body, + status=status, + content_type='application/json', + ) def _assert_program_data_present(self, response): """Verify that program data is present.""" @@ -283,7 +290,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): Verify that login is required to access the page. """ self.create_programs_config() - self._mock_programs_api() + self._mock_programs_api(self.data) self.client.logout() @@ -310,7 +317,7 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): def test_page_routing(self): """Verify that the page can be hit with or without a program name in the URL.""" self.create_programs_config() - self._mock_programs_api() + self._mock_programs_api(self.data) response = self.client.get(self.details_page) self._assert_program_data_present(response) @@ -320,3 +327,17 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase): response = self.client.get(self.details_page + 'program_name/invalid/') self.assertEquals(response.status_code, 404) + + def test_404_if_no_data(self): + """Verify that the page 404s if no program data is found.""" + self.create_programs_config() + + self._mock_programs_api(self.data, status=404) + response = self.client.get(self.details_page) + self.assertEquals(response.status_code, 404) + + httpretty.reset() + + self._mock_programs_api({}) + response = self.client.get(self.details_page) + self.assertEquals(response.status_code, 404) diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py index 017ca8955d..60cc5db110 100644 --- a/lms/djangoapps/learner_dashboard/views.py +++ b/lms/djangoapps/learner_dashboard/views.py @@ -57,6 +57,10 @@ def program_details(request, program_id): raise Http404 program_data = utils.get_programs(request.user, program_id=program_id) + + if not program_data: + raise Http404 + program_data = utils.supplement_program_data(program_data, request.user) context = { diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index f9d7f7f6fa..771b1551c9 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -190,6 +190,7 @@ def show_cart(request): 'form_html': form_html, 'currency_symbol': settings.PAID_COURSE_REGISTRATION_CURRENCY[1], 'currency': settings.PAID_COURSE_REGISTRATION_CURRENCY[0], + 'enable_bulk_purchase': microsite.get_value('ENABLE_SHOPPING_CART_BULK_PURCHASE', True) } return render_to_response("shoppingcart/shopping_cart.html", context) diff --git a/lms/djangoapps/verify_student/tests/test_integration.py b/lms/djangoapps/verify_student/tests/test_integration.py index c2401b6231..c45c9cd350 100644 --- a/lms/djangoapps/verify_student/tests/test_integration.py +++ b/lms/djangoapps/verify_student/tests/test_integration.py @@ -32,7 +32,7 @@ class TestProfEdVerification(ModuleStoreTestCase): min_price=self.MIN_PRICE, suggested_prices='' ) - + purchase_workflow = "?purchase_workflow=single" self.urls = { 'course_modes_choose': reverse( 'course_modes_choose', @@ -42,7 +42,7 @@ class TestProfEdVerification(ModuleStoreTestCase): 'verify_student_start_flow': reverse( 'verify_student_start_flow', args=[unicode(self.course_key)] - ), + ) + purchase_workflow, } def test_start_flow(self): diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 3484d9af6e..21715ba979 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -334,6 +334,10 @@ class PayAndVerifyView(View): # Redirect the user to a more appropriate page if the # messaging won't make sense based on the user's # enrollment / payment / verification status. + sku_to_use = relevant_course_mode.sku + purchase_workflow = request.GET.get('purchase_workflow', 'single') + if purchase_workflow == 'bulk' and relevant_course_mode.bulk_sku: + sku_to_use = relevant_course_mode.bulk_sku redirect_response = self._redirect_if_necessary( message, already_verified, @@ -342,7 +346,7 @@ class PayAndVerifyView(View): course_key, user_is_trying_to_pay, request.user, - relevant_course_mode.sku + sku_to_use ) if redirect_response is not None: return redirect_response diff --git a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js index 5fa267b361..b7de0bc487 100644 --- a/lms/static/js/spec/learner_dashboard/course_card_view_spec.js +++ b/lms/static/js/spec/learner_dashboard/course_card_view_spec.js @@ -61,7 +61,7 @@ define([ expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name); expect(view.$('.course-details .course-title-link').attr('href')).toEqual( - context.run_modes[0].marketing_url); + context.run_modes[0].course_url); expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); expect(view.$('.course-details .course-text .run-period').html()) .toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date); @@ -71,7 +71,7 @@ define([ expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name); expect(view.$('.course-details .course-title-link').attr('href')).toEqual( - context.run_modes[0].marketing_url); + context.run_modes[0].course_url); expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); expect(view.$('.course-details .course-text .run-period').html()).not.toBeDefined(); }); diff --git a/lms/static/js/student_account/views/FinishAuthView.js b/lms/static/js/student_account/views/FinishAuthView.js index 8ec1da6ab7..8e5c3955da 100644 --- a/lms/static/js/student_account/views/FinishAuthView.js +++ b/lms/static/js/student_account/views/FinishAuthView.js @@ -51,7 +51,8 @@ enrollmentAction: $.url( '?enrollment_action' ), courseId: $.url( '?course_id' ), courseMode: $.url( '?course_mode' ), - emailOptIn: $.url( '?email_opt_in' ) + emailOptIn: $.url( '?email_opt_in' ), + purchaseWorkflow: $.url( '?purchase_workflow' ) }; for (var key in queryParams) { if (queryParams[key]) { @@ -63,6 +64,7 @@ this.courseMode = queryParams.courseMode; this.emailOptIn = queryParams.emailOptIn; this.nextUrl = this.urls.defaultNextUrl; + this.purchaseWorkflow = queryParams.purchaseWorkflow; if (queryParams.next) { // Ensure that the next URL is internal for security reasons if ( ! window.isExternal( queryParams.next ) ) { diff --git a/lms/templates/learner_dashboard/course_card.underscore b/lms/templates/learner_dashboard/course_card.underscore index 1f4a923d15..01ba2dc4f4 100644 --- a/lms/templates/learner_dashboard/course_card.underscore +++ b/lms/templates/learner_dashboard/course_card.underscore @@ -1,6 +1,6 @@