diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py
index d4147b6fa3..043e4e32d8 100644
--- a/common/djangoapps/course_modes/views.py
+++ b/common/djangoapps/course_modes/views.py
@@ -94,6 +94,7 @@ class ChooseModeView(View):
"error": error,
"upgrade": upgrade,
"can_audit": "audit" in modes,
+ "responsive": True
}
if "verified" in modes:
context["suggested_prices"] = [
diff --git a/common/djangoapps/student/tests/test_login.py b/common/djangoapps/student/tests/test_login.py
index b35be740bf..eb3183a4a6 100644
--- a/common/djangoapps/student/tests/test_login.py
+++ b/common/djangoapps/student/tests/test_login.py
@@ -485,6 +485,7 @@ class LoginOAuthTokenMixin(object):
self._setup_user_response(success=True)
response = self.client.post(self.url, {"access_token": "dummy"})
self.assertEqual(response.status_code, 204)
+ self.assertEqual(self.client.session['_auth_user_id'], self.user.id)
def test_invalid_token(self):
self._setup_user_response(success=False)
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 1ef2defca4..3bf2fde715 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -265,6 +265,7 @@ class DashboardTest(ModuleStoreTestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@patch('courseware.views.log.warning')
+ @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
def test_blocked_course_scenario(self, log_warning):
self.client.login(username="jack", password="test")
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 388a2b8bfe..734f0c3e62 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -1114,6 +1114,7 @@ def login_user(request, error=""): # pylint: disable-msg=too-many-statements,un
}) # TODO: this should be status code 400 # pylint: disable=fixme
+@csrf_exempt
@require_POST
@social_utils.strategy("social:complete")
def login_oauth_token(request, backend):
@@ -1134,6 +1135,7 @@ def login_oauth_token(request, backend):
pass
# do_auth can return a non-User object if it fails
if user and isinstance(user, User):
+ login(request, user)
return JsonResponse(status=204)
else:
# Ensure user does not re-enter the pipeline
diff --git a/common/static/js/spec_helpers/rwd_header_footer.js b/common/static/js/spec_helpers/rwd_header_footer.js
new file mode 100644
index 0000000000..f68b97c8c7
--- /dev/null
+++ b/common/static/js/spec_helpers/rwd_header_footer.js
@@ -0,0 +1,99 @@
+/**
+ * Adds rwd classes and click handlers.
+ */
+
+(function($) {
+ 'use strict';
+
+ var rwd = (function() {
+
+ var _fn = {
+ header: 'header.global-new',
+
+ footer: '.edx-footer-new',
+
+ resultsUrl: 'course-search',
+
+ init: function() {
+ _fn.$header = $( _fn.header );
+ _fn.$footer = $( _fn.footer );
+ _fn.$nav = _fn.$header.find('nav');
+ _fn.$globalNav = _fn.$nav.find('.nav-global');
+
+ _fn.add.elements();
+ _fn.add.classes();
+ _fn.eventHandlers.init();
+ },
+
+ add: {
+ classes: function() {
+ // Add any RWD-specific classes
+ _fn.$header.addClass('rwd');
+ _fn.$footer.addClass('rwd');
+ },
+
+ elements: function() {
+ _fn.add.burger();
+ _fn.add.registerLink();
+ },
+
+ burger: function() {
+ _fn.$nav.prepend([
+ ''
+ ].join(''));
+ },
+
+ registerLink: function() {
+ var $register = _fn.$nav.find('.cta-register'),
+ $li = {},
+ $a = {},
+ count = 0;
+
+ // Add if register link is shown
+ if ( $register.length > 0 ) {
+ count = _fn.$globalNav.find('li').length + 1;
+
+ // Create new li
+ $li = $('
');
+ $li.addClass('desktop-hide nav-global-0' + count);
+
+ // Clone register link and remove classes
+ $a = $register.clone();
+ $a.removeClass();
+
+ // append to DOM
+ $a.appendTo( $li );
+ _fn.$globalNav.append( $li );
+ }
+ }
+ },
+
+ eventHandlers: {
+ init: function() {
+ _fn.eventHandlers.click();
+ },
+
+ click: function() {
+ // Toggle menu
+ _fn.$nav.on( 'click', '.mobile-menu-button', _fn.toggleMenu );
+ }
+ },
+
+ toggleMenu: function( event ) {
+ event.preventDefault();
+
+ _fn.$globalNav.toggleClass('show');
+ }
+ };
+
+ return {
+ init: _fn.init
+ };
+ })();
+
+ setTimeout( function() {
+ rwd.init();
+ }, 100);
+})(jQuery);
diff --git a/lms/djangoapps/courseware/tests/test_microsites.py b/lms/djangoapps/courseware/tests/test_microsites.py
index 95c68f8689..baa872f902 100644
--- a/lms/djangoapps/courseware/tests/test_microsites.py
+++ b/lms/djangoapps/courseware/tests/test_microsites.py
@@ -10,7 +10,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from helpers import LoginEnrollmentTestCase
from courseware.tests.modulestore_config import TEST_DATA_MIXED_MODULESTORE
-
+from course_modes.models import CourseMode
from xmodule.course_module import (
CATALOG_VISIBILITY_CATALOG_AND_ABOUT, CATALOG_VISIBILITY_NONE)
@@ -56,6 +56,7 @@ class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase):
self.course_with_visibility = CourseFactory.create(
display_name='visible_course',
org='TestMicrositeX',
+ course="foo",
catalog_visibility=CATALOG_VISIBILITY_CATALOG_AND_ABOUT,
)
@@ -190,3 +191,33 @@ class TestMicrosites(ModuleStoreTestCase, LoginEnrollmentTestCase):
url = reverse('about_course', args=[self.course_hidden_visibility.id.to_deprecated_string()])
resp = self.client.get(url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
self.assertEqual(resp.status_code, 404)
+
+ @override_settings(SITE_NAME=settings.MICROSITE_TEST_HOSTNAME)
+ def test_paid_course_registration(self):
+ """
+ Make sure that Microsite overrides on the ENABLE_SHOPPING_CART and
+ ENABLE_PAID_COURSE_ENROLLMENTS are honored
+ """
+ course_mode = CourseMode(
+ course_id=self.course_with_visibility.id,
+ mode_slug="honor",
+ mode_display_name="honor cert",
+ min_price=10,
+ )
+ course_mode.save()
+
+ # first try on the non microsite, which
+ # should pick up the global configuration (where ENABLE_PAID_COURSE_REGISTRATIONS = False)
+ url = reverse('about_course', args=[self.course_with_visibility.id.to_deprecated_string()])
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, 200)
+ self.assertIn("Register for {}".format(self.course_with_visibility.id.course), resp.content)
+ self.assertNotIn("Add {} to Cart ($10)".format(self.course_with_visibility.id.course), resp.content)
+
+ # now try on the microsite
+ url = reverse('about_course', args=[self.course_with_visibility.id.to_deprecated_string()])
+ resp = self.client.get(url, HTTP_HOST=settings.MICROSITE_TEST_HOSTNAME)
+ self.assertEqual(resp.status_code, 200)
+ self.assertNotIn("Register for {}".format(self.course_with_visibility.id.course), resp.content)
+ self.assertIn("Add {} to Cart ($10)".format(self.course_with_visibility.id.course), resp.content)
+ self.assertIn('$("#add_to_cart_post").click', resp.content)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 002dd5f51e..54936d972b 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -49,6 +49,7 @@ from xmodule.tabs import CourseTabList, StaffGradingTab, PeerGradingTab, OpenEnd
from xmodule.x_module import STUDENT_VIEW
import shoppingcart
from shoppingcart.models import CourseRegistrationCode
+from shoppingcart.utils import is_shopping_cart_enabled
from opaque_keys import InvalidKeyError
from microsite_configuration import microsite
@@ -731,8 +732,9 @@ def course_about(request, course_id):
registration_price = 0
in_cart = False
reg_then_add_to_cart_link = ""
- if (settings.FEATURES.get('ENABLE_SHOPPING_CART') and
- settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')):
+
+ _is_shopping_cart_enabled = is_shopping_cart_enabled()
+ if (_is_shopping_cart_enabled):
registration_price = CourseMode.min_course_price_for_currency(course_key,
settings.PAID_COURSE_REGISTRATION_CURRENCY[0])
if request.user.is_authenticated():
@@ -774,6 +776,8 @@ def course_about(request, course_id):
# We do not want to display the internal courseware header, which is used when the course is found in the
# context. This value is therefor explicitly set to render the appropriate header.
'disable_courseware_header': True,
+ 'is_shopping_cart_enabled': _is_shopping_cart_enabled,
+ 'cart_link': reverse('shoppingcart.views.show_cart'),
})
diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py
index 50d2bf2545..a9900889c1 100644
--- a/lms/djangoapps/instructor/tests/test_api.py
+++ b/lms/djangoapps/instructor/tests/test_api.py
@@ -1636,6 +1636,7 @@ class TestInstructorAPILevelsAccess(ModuleStoreTestCase, LoginEnrollmentTestCase
@ddt.ddt
@override_settings(MODULESTORE=TEST_DATA_MONGO_MODULESTORE)
+@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test endpoints that show data without side effects.
diff --git a/lms/djangoapps/shoppingcart/context_processor.py b/lms/djangoapps/shoppingcart/context_processor.py
index e884bef7e3..5fc9439025 100644
--- a/lms/djangoapps/shoppingcart/context_processor.py
+++ b/lms/djangoapps/shoppingcart/context_processor.py
@@ -5,9 +5,9 @@ navigation. We want to do this in the context_processor to
1) keep database accesses out of templates (this led to a transaction bug with user email changes)
2) because navigation.html is "called" by being included in other templates, there's no "views.py" to put this.
"""
-from django.conf import settings
-import shoppingcart
-from microsite_configuration import microsite
+
+from .models import Order, PaidCourseRegistration, CourseRegCodeItem
+from .utils import is_shopping_cart_enabled
def user_has_cart_context_processor(request):
@@ -19,20 +19,12 @@ def user_has_cart_context_processor(request):
display_shopping_cart = (
# user is logged in and
request.user.is_authenticated() and
- # settings enable paid course reg
- microsite.get_value(
- 'ENABLE_PAID_COURSE_REGISTRATION',
- settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')
- ) and
- # settings enable shopping cart
- microsite.get_value(
- 'ENABLE_SHOPPING_CART',
- settings.FEATURES.get('ENABLE_SHOPPING_CART')
- ) and
+ # do we have the feature turned on
+ is_shopping_cart_enabled() and
# user's cart has PaidCourseRegistrations
- shoppingcart.models.Order.user_cart_has_items(
+ Order.user_cart_has_items(
request.user,
- [shoppingcart.models.PaidCourseRegistration, shoppingcart.models.CourseRegCodeItem]
+ [PaidCourseRegistration, CourseRegCodeItem]
)
)
diff --git a/lms/djangoapps/shoppingcart/decorators.py b/lms/djangoapps/shoppingcart/decorators.py
new file mode 100644
index 0000000000..9f4366f8bb
--- /dev/null
+++ b/lms/djangoapps/shoppingcart/decorators.py
@@ -0,0 +1,22 @@
+"""
+This file defines any decorators used by the shopping cart app
+"""
+
+from django.http import Http404
+from .utils import is_shopping_cart_enabled
+
+
+def enforce_shopping_cart_enabled(func):
+ """
+ Is a decorator that forces a wrapped method to be run in a runtime
+ which has the ENABLE_SHOPPING_CART flag set
+ """
+ def func_wrapper(*args, **kwargs):
+ """
+ Wrapper function that does the enforcement that
+ the shopping cart feature is enabled
+ """
+ if not is_shopping_cart_enabled():
+ raise Http404
+ return func(*args, **kwargs)
+ return func_wrapper
diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py
index d06624b2c1..f15cb3f468 100644
--- a/lms/djangoapps/shoppingcart/models.py
+++ b/lms/djangoapps/shoppingcart/models.py
@@ -352,6 +352,9 @@ class Order(models.Model):
"""
if self.status == 'purchased':
+ log.error(
+ u"`purchase` method called on order {}, but order is already purchased.".format(self.id) # pylint: disable=E1101
+ )
return
self.status = 'purchased'
self.purchase_time = datetime.now(pytz.utc)
diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource2.py b/lms/djangoapps/shoppingcart/processors/CyberSource2.py
index 2794e03448..b20719ad0a 100644
--- a/lms/djangoapps/shoppingcart/processors/CyberSource2.py
+++ b/lms/djangoapps/shoppingcart/processors/CyberSource2.py
@@ -367,6 +367,8 @@ def _payment_accepted(order_id, auth_amount, currency, decision):
total_cost_currency=order.currency
)
)
+
+ #pylint: disable=attribute-defined-outside-init
ex.order = order
raise ex
else:
diff --git a/lms/djangoapps/shoppingcart/tests/test_views.py b/lms/djangoapps/shoppingcart/tests/test_views.py
index 873960f068..aed264158f 100644
--- a/lms/djangoapps/shoppingcart/tests/test_views.py
+++ b/lms/djangoapps/shoppingcart/tests/test_views.py
@@ -64,6 +64,7 @@ MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, incl
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class ShoppingCartViewsTests(ModuleStoreTestCase):
def setUp(self):
patcher = patch('student.models.tracker')
@@ -963,8 +964,36 @@ class ShoppingCartViewsTests(ModuleStoreTestCase):
((template, _context), _tmp) = render_mock.call_args
self.assertEqual(template, cert_item.single_item_receipt_template)
+ def _assert_404(self, url, use_post=False):
+ """
+ Helper method to assert that a given url will return a 404 status code
+ """
+ if use_post:
+ response = self.client.post(url)
+ else:
+ response = self.client.get(url)
+ self.assertEquals(response.status_code, 404)
+
+ @patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': False})
+ def test_disabled_paid_courses(self):
+ """
+ Assert that the pages that require ENABLE_PAID_COURSE_REGISTRATION=True return a
+ HTTP 404 status code when we have this flag turned off
+ """
+ self.login_user()
+ self._assert_404(reverse('shoppingcart.views.show_cart', args=[]))
+ self._assert_404(reverse('shoppingcart.views.clear_cart', args=[]))
+ self._assert_404(reverse('shoppingcart.views.remove_item', args=[]), use_post=True)
+ self._assert_404(reverse('shoppingcart.views.register_code_redemption', args=["testing"]))
+ self._assert_404(reverse('shoppingcart.views.use_code', args=[]), use_post=True)
+ self._assert_404(reverse('shoppingcart.views.update_user_cart', args=[]))
+ self._assert_404(reverse('shoppingcart.views.reset_code_redemption', args=[]), use_post=True)
+ self._assert_404(reverse('shoppingcart.views.billing_details', args=[]))
+ self._assert_404(reverse('shoppingcart.views.register_courses', args=[]))
+
@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
class RegistrationCodeRedemptionCourseEnrollment(ModuleStoreTestCase):
"""
Test suite for RegistrationCodeRedemption Course Enrollments
diff --git a/lms/djangoapps/shoppingcart/urls.py b/lms/djangoapps/shoppingcart/urls.py
index 02776da0a0..2ae1f408a4 100644
--- a/lms/djangoapps/shoppingcart/urls.py
+++ b/lms/djangoapps/shoppingcart/urls.py
@@ -6,23 +6,19 @@ urlpatterns = patterns('shoppingcart.views', # nopep8
url(r'^receipt/(?P[0-9]*)/$', 'show_receipt'),
url(r'^donation/$', 'donate', name='donation'),
url(r'^csv_report/$', 'csv_report', name='payment_csv_report'),
+ # These following URLs are only valid if the ENABLE_SHOPPING_CART feature flag is set
+ url(r'^$', 'show_cart'),
+ url(r'^clear/$', 'clear_cart'),
+ url(r'^remove_item/$', 'remove_item'),
+ url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'),
+ url(r'^register/redeem/(?P[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'),
+ url(r'^use_code/$', 'use_code'),
+ url(r'^update_user_cart/$', 'update_user_cart'),
+ url(r'^reset_code_redemption/$', 'reset_code_redemption'),
+ url(r'^billing_details/$', 'billing_details', name='billing_details'),
+ url(r'^register_courses/$', 'register_courses'),
)
-if settings.FEATURES['ENABLE_SHOPPING_CART']:
- urlpatterns += patterns(
- 'shoppingcart.views',
- url(r'^$', 'show_cart'),
- url(r'^clear/$', 'clear_cart'),
- url(r'^remove_item/$', 'remove_item'),
- url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'),
- url(r'^register/redeem/(?P[0-9A-Za-z]+)/$', 'register_code_redemption', name='register_code_redemption'),
- url(r'^use_code/$', 'use_code'),
- url(r'^update_user_cart/$', 'update_user_cart'),
- url(r'^reset_code_redemption/$', 'reset_code_redemption'),
- url(r'^billing_details/$', 'billing_details', name='billing_details'),
- url(r'^register_courses/$', 'register_courses'),
- )
-
if settings.FEATURES.get('ENABLE_PAYMENT_FAKE'):
from shoppingcart.tests.payment_fake import PaymentFakeView
urlpatterns += patterns(
diff --git a/lms/djangoapps/shoppingcart/utils.py b/lms/djangoapps/shoppingcart/utils.py
new file mode 100644
index 0000000000..de469f29be
--- /dev/null
+++ b/lms/djangoapps/shoppingcart/utils.py
@@ -0,0 +1,24 @@
+"""
+Utility methods for the Shopping Cart app
+"""
+
+from django.conf import settings
+from microsite_configuration import microsite
+
+
+def is_shopping_cart_enabled():
+ """
+ Utility method to check the various configuration to verify that
+ all of the settings have been enabled
+ """
+ enable_paid_course_registration = microsite.get_value(
+ 'ENABLE_PAID_COURSE_REGISTRATION',
+ settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION')
+ )
+
+ enable_shopping_cart = microsite.get_value(
+ 'ENABLE_SHOPPING_CART',
+ settings.FEATURES.get('ENABLE_SHOPPING_CART')
+ )
+
+ return (enable_paid_course_registration and enable_shopping_cart)
diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py
index 24cfd56fcc..fc7eab58cd 100644
--- a/lms/djangoapps/shoppingcart/views.py
+++ b/lms/djangoapps/shoppingcart/views.py
@@ -33,7 +33,7 @@ from .exceptions import (
)
from .models import (
Order, OrderTypes,
- PaidCourseRegistration, OrderItem, Coupon, CourseRegCodeItem,
+ PaidCourseRegistration, OrderItem, Coupon,
CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption,
Donation, DonationConfiguration
)
@@ -44,6 +44,7 @@ from .processors import (
import json
from xmodule_django.models import CourseKeyField
+from .decorators import enforce_shopping_cart_enabled
log = logging.getLogger("shoppingcart")
AUDIT_LOG = logging.getLogger("audit")
@@ -94,6 +95,7 @@ def add_course_to_cart(request, course_id):
@login_required
+@enforce_shopping_cart_enabled
def update_user_cart(request):
"""
when user change the number-of-students from the UI then
@@ -127,6 +129,7 @@ def update_user_cart(request):
@login_required
+@enforce_shopping_cart_enabled
def show_cart(request):
"""
This view shows cart items.
@@ -158,6 +161,7 @@ def show_cart(request):
@login_required
+@enforce_shopping_cart_enabled
def clear_cart(request):
cart = Order.get_cart_for_user(request.user)
cart.clear()
@@ -175,6 +179,7 @@ def clear_cart(request):
@login_required
+@enforce_shopping_cart_enabled
def remove_item(request):
"""
This will remove an item from the user cart and also delete the corresponding coupon codes redemption.
@@ -228,6 +233,7 @@ def remove_code_redemption(order_item_course_id, item_id, item, user):
@login_required
+@enforce_shopping_cart_enabled
def reset_code_redemption(request):
"""
This method reset the code redemption from user cart items.
@@ -240,6 +246,7 @@ def reset_code_redemption(request):
@login_required
+@enforce_shopping_cart_enabled
def use_code(request):
"""
This method may generate the discount against valid coupon code
@@ -292,6 +299,7 @@ def get_reg_code_validity(registration_code, request, limiter):
@require_http_methods(["GET", "POST"])
@login_required
+@enforce_shopping_cart_enabled
def register_code_redemption(request, registration_code):
"""
This view allows the student to redeem the registration code
@@ -383,6 +391,7 @@ def use_coupon_code(coupons, user):
@login_required
+@enforce_shopping_cart_enabled
def register_courses(request):
"""
This method enroll the user for available course(s)
@@ -519,6 +528,7 @@ def postpay_callback(request):
@require_http_methods(["GET", "POST"])
@login_required
+@enforce_shopping_cart_enabled
def billing_details(request):
"""
This is the view for capturing additional billing details
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 95cd064d46..6442bcf9d7 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1026,6 +1026,7 @@ main_vendor_js = base_vendor_js + [
dashboard_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/dashboard/**/*.js'))
discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js'))
+rwd_header_footer_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/common_helpers/rwd_header_footer.js'))
staff_grading_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/staff_grading/**/*.js'))
open_ended_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/open_ended/**/*.js'))
notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js'))
@@ -1204,6 +1205,10 @@ PIPELINE_JS = {
'source_filenames': dashboard_js,
'output_filename': 'js/dashboard.js'
},
+ 'rwd_header_footer': {
+ 'source_filenames': rwd_header_footer_js,
+ 'output_filename': 'js/rwd_header_footer.js'
+ },
'student_account': {
'source_filenames': student_account_js,
'output_filename': 'js/student_account.js'
diff --git a/lms/envs/test.py b/lms/envs/test.py
index 049fdf7b89..2810337e63 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -352,6 +352,8 @@ MICROSITE_CONFIGURATION = {
"ALWAYS_REDIRECT_HOMEPAGE_TO_DASHBOARD_FOR_AUTHENTICATED_USER": False,
"COURSE_CATALOG_VISIBILITY_PERMISSION": "see_in_catalog",
"COURSE_ABOUT_VISIBILITY_PERMISSION": "see_about_page",
+ "ENABLE_SHOPPING_CART": True,
+ "ENABLE_PAID_COURSE_REGISTRATION": True,
},
"default": {
"university": "default_university",
diff --git a/lms/static/images/verified-ribbon.png b/lms/static/images/verified-ribbon.png
new file mode 100644
index 0000000000..56b4b04b51
Binary files /dev/null and b/lms/static/images/verified-ribbon.png differ
diff --git a/lms/static/sass/base/_grid-settings.scss b/lms/static/sass/base/_grid-settings.scss
new file mode 100644
index 0000000000..4e41d3ba82
--- /dev/null
+++ b/lms/static/sass/base/_grid-settings.scss
@@ -0,0 +1,14 @@
+@import "neat/neat-helpers"; // or "neat-helpers" when in Rails
+
+/* Change the grid settings */
+$max-width: 1200px;
+/* Override the default global box-sizing */
+$border-box-sizing: false;
+
+
+
+/* Breakpoints */
+$mobile: new-breakpoint(max-width 320px 4);
+$tablet: new-breakpoint(min-width 321px max-width 768px, 8);
+$desktop: new-breakpoint(min-width 769px 12);
+$xl-desktop: new-breakpoint(min-width 980px 12);
diff --git a/lms/static/sass/base/_variables.scss b/lms/static/sass/base/_variables.scss
index 35dfbf1aa3..546af6040f 100644
--- a/lms/static/sass/base/_variables.scss
+++ b/lms/static/sass/base/_variables.scss
@@ -173,6 +173,7 @@ $m-blue-d1: #1790C7;
$m-blue-d2: #1580B0;
$m-blue-d3: #126F9A;
$m-blue-d4: #0A4A67;
+$m-blue-d5: #009EE7;
$m-blue-t0: rgba($m-blue,0.125);
$m-blue-t1: rgba($m-blue,0.25);
$m-blue-t2: rgba($m-blue,0.50);
@@ -423,6 +424,7 @@ $header-sans-serif: 'Open Sans', Arial, Helvetica, sans-serif;
$msg-bg: $action-primary-bg;
+
// New Shopping Cart
$dark-gray1: #4a4a4a;
diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss
index 9a6e4c37b2..9afd513a8b 100644
--- a/lms/static/sass/shared/_footer.scss
+++ b/lms/static/sass/shared/_footer.scss
@@ -1,10 +1,14 @@
// Open edX: LMS footer
// ====================
+@import '../base/grid-settings';
+@import 'neat/neat'; // lib - Neat
+
.wrapper-footer {
box-shadow: 0 -1px 5px 0 rgba(0,0,0, 0.1);
border-top: 1px solid tint($m-gray,50%);
padding: 25px ($baseline/2) ($baseline*1.5) ($baseline/2);
background: $footer-bg;
+ clear: both;
footer {
@include clearfix();
@@ -280,8 +284,6 @@ $edx-footer-bg-color: rgb(252,252,252);
@extend %t-weight4;
}
}
-
-
}
.edx-footer-new {
@@ -352,6 +354,7 @@ $edx-footer-bg-color: rgb(252,252,252);
.footer-nav-title {
@extend %edx-footer-title;
+ margin-top: $baseline;
}
.footer-nav-links {
@@ -372,12 +375,14 @@ $edx-footer-bg-color: rgb(252,252,252);
.footer-follow-title {
@extend %edx-footer-title;
+ margin-top: $baseline;
}
.footer-follow-links {
a {
@extend %edx-footer-link;
+ margin-top: 20px;
.icon, .copy {
display: inline-block;
@@ -397,4 +402,33 @@ $edx-footer-bg-color: rgb(252,252,252);
}
}
}
+
+ &.rwd {
+ @include box-sizing(border-box);
+ @include outer-container;
+
+ &.wrapper-footer footer {
+ min-width: 0;
+ }
+
+ .footer-about,
+ .footer-nav,
+ .footer-follow {
+ @include span-columns(12);
+ }
+
+ @include media( $tablet ) {
+ }
+
+ @include media( $desktop ) {
+ .footer-about {
+ @include span-columns(6);
+ }
+
+ .footer-nav,
+ .footer-follow {
+ @include span-columns(3);
+ }
+ }
+ }
}
diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss
index 33f7b7771f..dd0bddeb3d 100644
--- a/lms/static/sass/shared/_header.scss
+++ b/lms/static/sass/shared/_header.scss
@@ -1,3 +1,6 @@
+@import '../base/grid-settings';
+@import 'neat/neat'; // lib - Neat
+
header.global {
border-bottom: 1px solid $m-gray;
box-shadow: 0 1px 5px 0 rgba(0,0,0, 0.1);
@@ -317,7 +320,6 @@ header.global {
.view-courses .nav-global-02,
.view-schools .nav-global-03,
.view-register .nav-global-04 {
-
a {
text-decoration: none;
color: $link-color !important;
@@ -331,8 +333,10 @@ header.global {
// CASE: marketing/course discovery
header.global-new {
@extend %ui-depth1;
+ /* Temp. fix until applied globally */
+ @include box-sizing(border-box);
+
position: relative;
- height: ($baseline*3.75);
width: 100%;
border-bottom: 4px solid $courseware-border-bottom-color;
box-shadow: 0 1px 5px 0 rgba(0,0,0, 0.1);
@@ -340,15 +344,16 @@ header.global-new {
nav {
@include clearfix();
+ @include box-sizing(border-box);
width: grid-width(12);
- height: ($baseline*2);
+ height: 74px;
margin: 0 auto;
- padding: 18px ($baseline/2) 0;
+ padding: 17px 0;
}
h1.logo {
float: left;
- margin: -2px 39px 0px 0px;
+ margin: -2px 39px 0 10px;
position: relative;
a {
@@ -560,7 +565,7 @@ header.global-new {
}
}
- .nav-global {
+ %default-header-nav {
margin-top: ($baseline/4);
list-style: none;
float: left;
@@ -568,25 +573,21 @@ header.global-new {
li,
div {
display: inline-block;
- margin: 0 $baseline+1 0 0;
+ margin: 0;
text-transform: uppercase;
letter-spacing: 0 !important;
- &:last-child {
- margin-right: 0;
- }
-
a {
- border-bottom: 4px solid $header-bg;
display:block;
- padding: ($baseline/4);
+ padding: 3px 10px;
font-size: 18px;
- padding-bottom: ($baseline*1.25);
- font-weight: 600;
+ line-height: 24px;
+ font-weight: 500;
font-family: $header-sans-serif;
color: $courseware-navigation-color;
- &:hover, &:focus{
+ &:hover,
+ &:focus {
text-decoration: none;
color: $courseware-hover-color;
}
@@ -594,25 +595,26 @@ header.global-new {
}
}
+ .nav-global {
+ @extend %default-header-nav;
+ }
+
.nav-courseware {
- @extend .nav-global;
+ @extend %default-header-nav;
float: right;
div {
display: inline-block;
- margin: 0 21px 0 0;
text-transform: uppercase;
letter-spacing: 0!important;
position: relative;
- vertical-align: middle;
&:last-child {
- margin-right: 0;
+ margin-right: 10px;
}
a {
&.nav-courseware-button {
- padding: 5px 45px 5px 45px;
border: 3px solid $courseware-button-border-color;
border-radius: 5px;
margin-top: -22px;
@@ -628,6 +630,182 @@ header.global-new {
}
}
}
+
+ &.rwd {
+ nav {
+ max-width: 1180px;
+ width: 100%;
+ }
+
+ .mobile-menu-button {
+ @extend %t-action1;
+ display: inline;
+ float: left;
+ text-decoration: none;
+ color: $m-gray;
+ margin-top: 9px;
+
+ &:hover,
+ &:active,
+ &:focus {
+ text-decoration: none;
+ }
+ }
+
+ .logo {
+ position: absolute;
+ width: 54px;
+ left: calc( 50% - 90px );
+ top: 20px;
+
+ img {
+ width: 54px;
+ }
+ }
+
+ .nav-global,
+ .nav-courseware {
+ a {
+ @extend %t-action3;
+
+ &.nav-courseware-button {
+ width: 86px;
+ text-align: center;
+ margin-top: -3px;
+ }
+ }
+ }
+
+ .nav-global,
+ .nav-courseware-01 {
+ display: none;
+ }
+
+ .nav-global {
+ position: absolute;
+ top: 73px;
+ left: calc( 50% - 160px );
+ z-index: 1000;
+ width: 320px;
+ background: $m-blue-d3;
+
+ &.show {
+ display: inline;
+ }
+
+ a {
+ color: white;
+ padding: 10px;
+ font-weight: 300;
+
+ &:hover,
+ &:focus {
+ background: $m-blue-d5;
+ color: white;
+ border-bottom: none;
+ }
+ }
+
+ li {
+ display: block;
+ border-bottom: 1px solid $m-blue-d5;
+ }
+ }
+
+ .nav-courseware {
+ display: inline;
+
+ div:last-child {
+ margin-right: 0;
+ }
+ }
+
+ @include media( 320px ) {
+ nav {
+ width: 320px;
+ }
+ }
+
+ @include media( $desktop ) {
+ nav {
+ width: 100%;
+ }
+
+ .mobile-menu-button {
+ display: none;
+ }
+
+ .logo {
+ position: relative;
+ width: auto;
+ top: inherit;
+ left: inherit;
+ margin-left: 10px;
+
+ img {
+ width: auto;
+ }
+ }
+
+ .nav-global {
+ display: inline;
+ position: relative;
+ z-index: auto;
+ width: auto;
+ top: auto;
+ left: auto;
+ background: inherit;
+
+ a {
+ color: $courseware-navigation-color;
+ padding: 3px 10px;
+ font-weight: 500;
+
+ &:hover,
+ &:focus {
+ background: inherit;
+ color: $courseware-hover-color;
+ }
+ }
+
+ li {
+ display: inline-block;
+ border-bottom: none;
+ }
+ }
+
+ .nav-courseware {
+ div:last-child {
+ margin-right: 10px;
+ }
+ }
+
+ .nav-courseware-01 {
+ display: inline-block;
+ }
+
+ .desktop-hide {
+ display: none!important;
+ }
+ }
+
+ @include media( $xl-desktop ) {
+ nav {
+ padding: 17px 10px;
+ }
+
+ .nav-global,
+ .nav-courseware {
+ a {
+ font-size: 18px;
+ }
+ }
+
+ .logo {
+ margin-left: 0;
+ }
+ }
+ }
}
.view-register header.global-new .cta-register {
diff --git a/lms/static/sass/views/_verification.scss b/lms/static/sass/views/_verification.scss
index 3945876888..b900784744 100644
--- a/lms/static/sass/views/_verification.scss
+++ b/lms/static/sass/views/_verification.scss
@@ -1,5 +1,7 @@
// lms - views - verification flow
// ====================
+@import '../base/grid-settings';
+@import 'neat/neat'; // lib - Neat
// MISC: extends - button
%btn-verify-primary {
@@ -12,7 +14,6 @@
.is-expandable {
.title-expand {
-
}
.expandable-icon {
@@ -438,7 +439,6 @@
}
}
}
-
}
}
@@ -989,7 +989,7 @@
@extend %t-weight4;
position: absolute;
top: -($baseline*1.25);
- left: 45%;
+ left: calc( 50% - 46px );
padding: ($baseline/2) ($baseline*1.5);
background: white;
text-align: center;
@@ -1129,22 +1129,35 @@
}
.content-supplementary {
- width: flex-grid(12,12);
+ @include box-sizing(border-box);
+ @include outer-container;
+ @include span-columns(12);
.list-help {
@include clearfix();
.help-item {
- width: flex-grid(4,12);
+ @include fill-parent;
+
float: left;
margin-right: flex-gutter();
+ margin-bottom: 25px;
&:last-child {
- margin-right: 0;
+ margin: 0;
}
+ }
+ }
- &.help-item-technical {
- width: flex-grid(8,12);
+ @include media( 550px ) {
+ .list-help {
+ .help-item {
+ @include span-columns(4);
+ margin-bottom: 0;
+
+ &.help-item-technical {
+ @include span-columns(8);
+ }
}
}
}
@@ -1154,6 +1167,10 @@
// VIEW: select a track
&.step-select-track {
+ .container {
+ min-width: 0;
+ max-width: 1200px;
+ }
.sts-track {
@extend %text-sr;
@@ -1161,11 +1178,10 @@
.form-register-choose {
@include clearfix();
- width: flex-grid(12,12);
margin: ($baseline*2) 0;
.deco-divider {
- width: flex-grid(12,12);
+ @include fill-parent;
float: left;
}
}
@@ -1175,7 +1191,7 @@
}
.register-choice {
- width: flex-grid(12,12);
+ @include fill-parent;
margin: 0 flex-gutter() $baseline 0;
border-top: ($baseline/4) solid $m-gray-d4;
padding: $baseline ($baseline*1.5);
@@ -1190,28 +1206,35 @@
vertical-align: middle;
}
- .wrapper-copy {
- width: flex-grid(8,8);
- }
-
.list-actions {
- width: flex-grid(8,8);
+ @include fill-parent;
text-align: right;
+ float: right;
+ margin: ($baseline/4) 0;
+ border-top: none;
+ clear: both;
}
.title {
@extend %t-title5;
@extend %t-weight5;
margin-bottom: ($baseline/2);
+ width: calc( 100% - 30px );
}
.copy {
@extend %t-copy-base;
}
- .action-select input {
- @extend %t-weight4;
- padding: ($baseline/2) ($baseline*0.75);
+ .action-select {
+ @include fill-parent;
+
+ input {
+ @extend %t-weight4;
+ padding: ($baseline/2) ($baseline*0.75);
+ width: 100%;
+ white-space: normal;
+ }
}
}
@@ -1226,15 +1249,9 @@
display: block;
width: ($baseline*2.9);
height: ($baseline*4.2);
- background: transparent url('../images/honor-ribbon.png') no-repeat 0 0;
- }
-
- .wrapper-copy {
- width: flex-grid(8,8);
}
.list-actions {
- width: flex-grid(8,8);
margin: ($baseline) 0;
}
@@ -1249,19 +1266,12 @@
.deco-ribbon {
position: absolute;
- top: -($baseline*1.5);
+ top: -10px;
right: $baseline;
display: block;
- width: ($baseline*3);
- height: ($baseline*4);
- background: transparent url('../images/vcert-ribbon-s.png') no-repeat 0 0;
- }
-
- .list-actions {
- margin: ($baseline/4) 0;
- border-top: none;
- width: flex-grid(4,12);
- float: right;
+ width: 45px;
+ height: 45px;
+ background: transparent url('../images/verified-ribbon.png') no-repeat 0 0;
}
.action-intro, .action-select {
@@ -1270,15 +1280,11 @@
}
.action-intro {
+ @include fill-parent;
@extend %copy-detail;
- width: flex-grid(3,8);
text-align: left;
}
- .action-select {
- width: initial;
- }
-
.action-select input {
@extend %btn-verify-primary;
}
@@ -1301,7 +1307,7 @@
}
.help-register {
- width: flex-grid(4,12);
+ @include span-columns(4);
.title {
@extend %hd-lv4;
@@ -1333,8 +1339,8 @@
.contribution-options {
@include clearfix();
+ @include fill-parent;
margin: 0;
- width: flex-grid(8,12);
&:after{
clear: none;
@@ -1342,6 +1348,7 @@
}
.field {
+ @include fill-parent;
float: left;
margin: 0 ($baseline/2) ($baseline/2) 0;
padding: ($baseline/2) ($baseline*0.75);
@@ -1380,6 +1387,65 @@
}
}
}
+
+ @include media(min-width 550px max-width 768px) {
+ .contribution-options {
+ .field {
+ @include span-columns(6);
+
+ &:nth-of-type(even) {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .register-choice {
+ .list-actions {
+ float: left;
+ width: auto;
+ }
+
+ .action-select {
+ width: initial;
+
+ input {
+ width: initial;
+ }
+ }
+ }
+ }
+
+ @include media( $desktop ) {
+ .contribution-options {
+ .field {
+ width: auto;
+ }
+ }
+
+ .register-choice {
+ .list-actions {
+ @include span-columns(4);
+ width: auto;
+ }
+
+ .action-select {
+ width: initial;
+
+ input {
+ width: initial;
+ }
+ }
+ }
+ }
+
+ @include media( $xl-desktop ) {
+ .register-choice {
+ .list-actions {
+ float: right;
+ clear: none;
+ }
+ }
+ }
}
// VIEW: requirements
diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html
index 77063c099c..0859f2247b 100644
--- a/lms/templates/courseware/course_about.html
+++ b/lms/templates/courseware/course_about.html
@@ -4,11 +4,6 @@
from courseware.courses import course_image_url, get_course_about_section
from django.conf import settings
from edxmako.shortcuts import marketing_link
-
- if settings.FEATURES.get('ENABLE_SHOPPING_CART'):
- cart_link = reverse('shoppingcart.views.show_cart')
- else:
- cart_link = ""
%>
<%namespace name='static' file='../static_content.html'/>
<%! from microsite_configuration import microsite %>
@@ -42,7 +37,7 @@
event.preventDefault();
});
- % if settings.FEATURES.get('ENABLE_SHOPPING_CART') and settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION'):
+ % if is_shopping_cart_enabled:
add_course_complete_handler = function(jqXHR, textStatus) {
if (jqXHR.status == 200) {
location.href = "${cart_link}";
@@ -162,7 +157,7 @@
## so that they can register and become a real user that can enroll.
% elif not is_shib_course and not can_enroll:
${_("Enrollment is Closed")}
- %elif settings.FEATURES.get('ENABLE_PAID_COURSE_REGISTRATION') and registration_price:
+ %elif is_shopping_cart_enabled and registration_price:
<%
if user.is_authenticated():
reg_href = "#"
diff --git a/lms/templates/main.html b/lms/templates/main.html
index 51649afed1..0026085f3c 100644
--- a/lms/templates/main.html
+++ b/lms/templates/main.html
@@ -11,6 +11,9 @@
+ % if responsive:
+
+ % endif
<%! from django.utils.translation import ugettext as _ %>
<%! from microsite_configuration import microsite %>
<%! from microsite_configuration import page_title_breadcrumbs %>
diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html
index d5807a93a8..54cdfb7ba5 100644
--- a/lms/templates/navigation-edx.html
+++ b/lms/templates/navigation-edx.html
@@ -53,11 +53,15 @@ site_status_msg = get_site_status_msg(course_id)
% if user.is_authenticated():
diff --git a/lms/templates/verify_student/_verification_header.html b/lms/templates/verify_student/_verification_header.html
index 40dfa7dd1d..ce8a16e173 100644
--- a/lms/templates/verify_student/_verification_header.html
+++ b/lms/templates/verify_student/_verification_header.html
@@ -1,5 +1,7 @@
<%! from django.utils.translation import ugettext as _ %>
+<%namespace name='static' file='../static_content.html'/>
+
+
+<%block name="js_extra">
+ <%static:js group='rwd_header_footer'/>
+%block>