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'/> +