Merge branch 'master' into ibrahimahmed443/MAYN-280-explore-new-courses-button-fix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 ) ) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="grid-container grid-manual">
|
||||
<div class="course-meta-container col-12 md-col-8 sm-col-12">
|
||||
<a href="<%- marketing_url %>" class="course-image-link">
|
||||
<a href="<%- course_url %>" class="course-image-link">
|
||||
<img
|
||||
class="header-img"
|
||||
src="<%- course_image_url %>"
|
||||
@@ -8,7 +8,7 @@
|
||||
</a>
|
||||
<div class="course-details">
|
||||
<h3 class="course-title">
|
||||
<a href="<%- marketing_url %>" class="course-title-link">
|
||||
<a href="<%- course_url %>" class="course-title-link">
|
||||
<%- display_name %>
|
||||
</a>
|
||||
</h3>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</picture>
|
||||
<h2 class="hd-2 title"><%- name %></h2>
|
||||
<p class="subtitle"><%- subtitle %></p>
|
||||
<a href="" class="breadcrumb"><%- gettext('Programs') %></a>
|
||||
<a href="/dashboard/programs" class="breadcrumb"><%- gettext('Programs') %></a>
|
||||
<span><%- StringUtils.interpolate(
|
||||
gettext('{category}\'s program'),
|
||||
{category: category}
|
||||
|
||||
@@ -101,6 +101,7 @@ from openedx.core.lib.courses import course_image_url
|
||||
% endif
|
||||
</div>
|
||||
<div class="col-2">
|
||||
% if enable_bulk_purchase:
|
||||
<div class="numbers-row" aria-live="polite">
|
||||
<label for="field_${item.id}">${_('Students:')}</label>
|
||||
<div class="counter">
|
||||
@@ -116,8 +117,9 @@ from openedx.core.lib.courses import course_image_url
|
||||
</button>
|
||||
<!--<a name="updateBtn" class="updateBtn hidden" id="updateBtn-${item.id}" href="#">update</a>-->
|
||||
<span class="error-text hidden" id="students-${item.id}"></span>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<button href="#" class="btn-remove" data-item-id="${item.id}">
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"underscore.string": "~3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"edx-custom-a11y-rules": "edx/edx-custom-a11y-rules",
|
||||
"edx-custom-a11y-rules": "git+https://github.com/edx/edx-custom-a11y-rules.git#12d2cae4ffdbb45c5643819211e06c17d6200210",
|
||||
"pa11y": "3.6.0",
|
||||
"pa11y-reporter-1.0-json": "1.0.2",
|
||||
"jasmine-core": "^2.4.1",
|
||||
|
||||
Reference in New Issue
Block a user