From cc48800d545afb6a91ce0bf54e2c7d09eb7aba1e Mon Sep 17 00:00:00 2001 From: Adam Palay Date: Mon, 10 Nov 2014 11:05:56 -0500 Subject: [PATCH 1/8] add logging for orders when 'purchase' is called on them multiple times ECOM-630 --- lms/djangoapps/shoppingcart/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index 37a32dfe0c..1b33941a03 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) From bd87a9dd2955add49acba55e209a652f9081b671 Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Fri, 7 Nov 2014 11:14:12 -0500 Subject: [PATCH 2/8] Record processor response on both successful and unsuccessful orders. --- .../shoppingcart/processors/CyberSource2.py | 24 ++++++++++++++++++- .../shoppingcart/processors/exceptions.py | 1 + .../processors/tests/test_CyberSource2.py | 14 +++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource2.py b/lms/djangoapps/shoppingcart/processors/CyberSource2.py index 5606279829..1d66138a96 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource2.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource2.py @@ -79,6 +79,7 @@ def process_postpay_callback(params): 'error_html': '' } else: + _record_payment_info(params, result['order']) return { 'success': False, 'order': result['order'], @@ -86,6 +87,11 @@ def process_postpay_callback(params): } except CCProcessorException as error: log.exception('error processing CyberSource postpay callback') + # if we have the order and the id, log it + if hasattr(error, 'order'): + _record_payment_info(params, error.order) + else: + log.info(json.dumps(params)) return { 'success': False, 'order': None, # due to exception we may not have the order @@ -350,7 +356,7 @@ def _payment_accepted(order_id, auth_amount, currency, decision): 'order': order } else: - raise CCProcessorWrongAmountException( + ex = CCProcessorWrongAmountException( _( u"The amount charged by the processor {charged_amount} {charged_amount_currency} is different " u"than the total cost of the order {total_cost} {total_cost_currency}." @@ -361,6 +367,8 @@ def _payment_accepted(order_id, auth_amount, currency, decision): total_cost_currency=order.currency ) ) + ex.order = order + raise ex else: return { 'accepted': False, @@ -408,6 +416,20 @@ def _record_purchase(params, order): ) +def _record_payment_info(params, order): + """ + Record the purchase and run purchased_callbacks + + Args: + params (dict): The parameters we received from CyberSource. + + Returns: + None + """ + order.processor_reply_dump = json.dumps(params) + order.save() + + def _get_processor_decline_html(params): """ Return HTML indicating that the user's payment was declined. diff --git a/lms/djangoapps/shoppingcart/processors/exceptions.py b/lms/djangoapps/shoppingcart/processors/exceptions.py index 2bcf35e50f..47a51a8b60 100644 --- a/lms/djangoapps/shoppingcart/processors/exceptions.py +++ b/lms/djangoapps/shoppingcart/processors/exceptions.py @@ -16,5 +16,6 @@ class CCProcessorDataException(CCProcessorException): class CCProcessorWrongAmountException(CCProcessorException): pass + class CCProcessorUserCancelled(CCProcessorException): pass diff --git a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py index de3777222f..5a12fa5597 100644 --- a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py +++ b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py @@ -49,6 +49,13 @@ class CyberSource2Test(TestCase): line_cost=self.COST ) + def assert_dump_recorded(self, order): + """ + Verify that this order does have a dump of information from the + payment processor. + """ + self.assertNotEqual(order.processor_reply_dump, '') + def test_render_purchase_form_html(self): # Verify that the HTML form renders with the payment URL specified # in the test settings. @@ -131,6 +138,7 @@ class CyberSource2Test(TestCase): # Expect that the order has been marked as purchased self.assertEqual(result['order'].status, 'purchased') + self.assert_dump_recorded(result['order']) def test_process_payment_rejected(self): # Simulate a callback from CyberSource indicating that the payment was rejected @@ -140,6 +148,7 @@ class CyberSource2Test(TestCase): # Expect that we get an error message self.assertFalse(result['success']) self.assertIn(u"did not accept your payment", result['error_html']) + self.assert_dump_recorded(result['order']) def test_process_payment_invalid_signature(self): # Simulate a callback from CyberSource indicating that the payment was rejected @@ -167,6 +176,9 @@ class CyberSource2Test(TestCase): # Expect an error self.assertFalse(result['success']) self.assertIn(u"different amount than the order total", result['error_html']) + # refresh data for current order + order = Order.objects.get(id=self.order.id) + self.assert_dump_recorded(order) def test_process_amount_paid_not_decimal(self): # Change the payment amount to a non-decimal @@ -202,6 +214,7 @@ class CyberSource2Test(TestCase): msg="Payment was not successful: {error}".format(error=result.get('error_html')) ) self.assertEqual(result['error_html'], '') + self.assert_dump_recorded(result['order']) # Expect that the order has placeholders for the missing credit card digits self.assertEqual(result['order'].bill_to_ccnum, '####') @@ -233,6 +246,7 @@ class CyberSource2Test(TestCase): # Verify that this executes without a unicode error result = process_postpay_callback(params) self.assertTrue(result['success']) + self.assert_dump_recorded(result['order']) @ddt.data('string', u'üñîçø∂é') def test_get_processor_exception_html(self, error_string): From 58b4982d2da013039367418ab984c9bdadb2e6be Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 10 Nov 2014 12:08:28 -0500 Subject: [PATCH 3/8] harden down access to the shoppingcart if the ENABLE_PAID_COURSE_REGISTRATION flag is not set (either via the main Django configuration settings) or through microsite overrides --- common/djangoapps/student/tests/tests.py | 1 + lms/djangoapps/courseware/views.py | 5 ++-- lms/djangoapps/instructor/tests/test_api.py | 1 + .../shoppingcart/context_processor.py | 15 ++-------- lms/djangoapps/shoppingcart/decorators.py | 22 ++++++++++++++ .../shoppingcart/tests/test_views.py | 29 +++++++++++++++++++ lms/djangoapps/shoppingcart/urls.py | 26 +++++++---------- lms/djangoapps/shoppingcart/utils.py | 24 +++++++++++++++ lms/djangoapps/shoppingcart/views.py | 12 +++++++- 9 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 lms/djangoapps/shoppingcart/decorators.py create mode 100644 lms/djangoapps/shoppingcart/utils.py 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/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py index 6cb05bd0a9..cf44331ec7 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,8 @@ 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')): + + 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(): diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index feda24b885..b42c1dc3ad 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -1604,6 +1604,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..20e096155b 100644 --- a/lms/djangoapps/shoppingcart/context_processor.py +++ b/lms/djangoapps/shoppingcart/context_processor.py @@ -5,9 +5,8 @@ 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 def user_has_cart_context_processor(request): @@ -19,16 +18,8 @@ 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 + shoppingcart.utils.is_shopping_cart_enabled() and # user's cart has PaidCourseRegistrations shoppingcart.models.Order.user_cart_has_items( request.user, 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/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 53c72219ab..aa1985f95f 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. @@ -227,6 +232,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. @@ -239,6 +245,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 @@ -291,6 +298,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 @@ -382,6 +390,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) @@ -518,6 +527,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 From 9270c7c77d57631da64ece358a3f8610d97187cb Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Nov 2014 15:20:35 -0500 Subject: [PATCH 4/8] Make OAuth token login endpoint CSRF exempt --- common/djangoapps/student/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index c51f995012..177c37dc84 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): From 23081f076b27b427720adb80dd7b0ff28ac4be7c Mon Sep 17 00:00:00 2001 From: AlasdairSwan Date: Fri, 31 Oct 2014 09:33:17 -0400 Subject: [PATCH 5/8] ECOM-574 Updated track selection page to be responsive --- common/djangoapps/course_modes/views.py | 1 + .../js/spec_helpers/rwd_header_footer.js | 99 ++++++++ lms/envs/common.py | 5 + lms/static/images/verified-ribbon.png | Bin 0 -> 1791 bytes lms/static/sass/base/_grid-settings.scss | 14 ++ lms/static/sass/base/_variables.scss | 2 + lms/static/sass/shared/_footer.scss | 38 ++- lms/static/sass/shared/_header.scss | 220 ++++++++++++++++-- lms/static/sass/views/_verification.scss | 152 ++++++++---- lms/templates/main.html | 3 + lms/templates/navigation-edx.html | 14 +- .../verify_student/_verification_header.html | 6 + 12 files changed, 483 insertions(+), 71 deletions(-) create mode 100644 common/static/js/spec_helpers/rwd_header_footer.js create mode 100644 lms/static/images/verified-ribbon.png create mode 100644 lms/static/sass/base/_grid-settings.scss 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/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/envs/common.py b/lms/envs/common.py index 97900cd92a..c238c3efcd 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1019,6 +1019,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')) @@ -1197,6 +1198,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/static/images/verified-ribbon.png b/lms/static/images/verified-ribbon.png new file mode 100644 index 0000000000000000000000000000000000000000..56b4b04b5156026f3a4567ca5cdad209016c12f4 GIT binary patch literal 1791 zcmbVNc~BE)6yFd)l%TOVKs@3a$|0I#HwR=TKuicy491YQjGZEzWCIbB4V#t3Q~?d8 zSRD>KRVs>Y)sY#j0~Qr2qarwh%77!N)#5>|2Ud$XcvPz0aM=Fg^pEb$?)%>Nz2AH9 zd++yVH)~Rp0;bKL1^_@n@-n5ClCbCX^QFG*_|gVSu*k%8G7UG84wS$Exe?c6V6p|x z!n7D_T$k60#R335$E-^y)78rn18(7=9vhz1Vx!Oi5F78bp@tlc1oc>!*(&4w@zY5T zXg10?AB)ujwM~I#o0qu=EX|dwGq`dLQX?lm4vcjolmiP!qM*}~YqcXz8E4EdLaja9 zd=5BfLgvUg<4L8fHJ}0~Fi^}Ba}5F*0;8ooNGy^<&|(l4Ktetw;KNcb1RH0Z7F_!}D~UR}R(t5A z2PI}V5M~=`#;u^oBdW*Ok}?jJ>G%~aHnsY#W2=3lP*lnIPSnPScmlq~; zN=hh%0)cetBsSS=**J;YvvC`!P=JwY z)L^!H^o5@Oj9*uY5$1f%s3LF+I96c9{Eh@-P@^asMkQRagyM@RS|Z`1VgtrSg(hm> zU=YR##GDDd@&6Q#PbI|nFyvp1ncSji=vltyKc&2#Ak0dQ4MEMv6z@-&)YwHOE9E-p zHT~$DH}P#YU^%1Bd#lS>PYYVVZBsul^UnBwE z*ix!Z3TJnP?q#z^IyBEqBkMrL2_m+NeV9#?7L;MZie0LZs_-kq8S1K8} zGP{L3bsq0~za;c=4r}p?k$RVxcUjz$&ibca7h4}zihf(>E0$xvsX^0A*jrq_j+r?J zYdV(PR91&iJ96rW^5?g5E3&R#sMFk8IO5%}H;z1=$GF7nZi`9^SkWW?v3o;fP@AxK z*t2`VFxoBJ;eEJQ4IcFxDA4<|d>h#_O~J-& zNu)^-47KmS6Tu9oJ+Xorerx8=Xbx$3Pi#s@qkZ;7RJ7)Um))*xTf5()Kz~J_S(vqg=DAv0(INXHOyF)7GB0h*j|VrBBfH&)zT;4T-Z9hs=@mCH~iMl~+D; zltJkiQcU-eqalx{R^3}wej%%VRmHjbS2sGT=DeC;11S&Ip{1) zkN}==@A{>uH=rdcVL^W$In&)Ysw>&;SaGU29>|-@d`M5(GcTcq*4yDT5S1P{PkJ}O z#K@+2&_di4*>b_Y6aGa_zV~0;>sYgKy#~n%{ym@8>F~;9^w76HN(zP#HZ@m&*S~Pf zARBEzmI)xNFV(b2WLnYm@F+Ua)c|M%H(q5_HmUa@M8?2B-gChb*x?&TeRz;II- zGXlu-XQoWov1wsnXMi8M!y^bJ;oE(Fvv*%A-Z`@62K#4PS4)^~aCKqv&i!<8we_3+ suT)`oEo*jm#WW+?y%fP){Q)lq;6|Y7)o(sI;`t>dC#EV7EzR8YH+sdCZ~y=R literal 0 HcmV?d00001 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 4a149a134b..9f275e02a3 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/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 8a1cdadac2..780fd6b00b 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'/> + From db8dd8de64842429c19c06a64e48d50aa2e6bc4b Mon Sep 17 00:00:00 2001 From: Greg Price Date: Mon, 10 Nov 2014 18:05:41 -0500 Subject: [PATCH 6/8] Fix OAuth token login endpoint to set session user --- common/djangoapps/student/tests/test_login.py | 1 + common/djangoapps/student/views.py | 1 + 2 files changed, 2 insertions(+) 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/views.py b/common/djangoapps/student/views.py index 177c37dc84..0ebf4971dd 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -1135,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 From a93faad78bac5764fb19ba5347c0faa823cfc684 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Mon, 10 Nov 2014 18:57:23 -0500 Subject: [PATCH 7/8] try importing the exact references we need rather than deferencing it from the djangoapp module --- .../courseware/tests/test_microsites.py | 33 ++++++++++++++++++- lms/djangoapps/courseware/views.py | 5 ++- .../shoppingcart/context_processor.py | 9 ++--- lms/envs/test.py | 2 ++ lms/templates/courseware/course_about.html | 9 ++--- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lms/djangoapps/courseware/tests/test_microsites.py b/lms/djangoapps/courseware/tests/test_microsites.py index 2f34de8e14..b9a916dd8c 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) @@ -55,6 +55,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, ) @@ -189,3 +190,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 cf44331ec7..2ecaab3d37 100644 --- a/lms/djangoapps/courseware/views.py +++ b/lms/djangoapps/courseware/views.py @@ -733,7 +733,8 @@ def course_about(request, course_id): in_cart = False reg_then_add_to_cart_link = "" - if (is_shopping_cart_enabled()): + _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(): @@ -775,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/shoppingcart/context_processor.py b/lms/djangoapps/shoppingcart/context_processor.py index 20e096155b..5fc9439025 100644 --- a/lms/djangoapps/shoppingcart/context_processor.py +++ b/lms/djangoapps/shoppingcart/context_processor.py @@ -6,7 +6,8 @@ navigation. We want to do this in the context_processor to 2) because navigation.html is "called" by being included in other templates, there's no "views.py" to put this. """ -import shoppingcart +from .models import Order, PaidCourseRegistration, CourseRegCodeItem +from .utils import is_shopping_cart_enabled def user_has_cart_context_processor(request): @@ -19,11 +20,11 @@ def user_has_cart_context_processor(request): # user is logged in and request.user.is_authenticated() and # do we have the feature turned on - shoppingcart.utils.is_shopping_cart_enabled() and + 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/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/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 = "#" From da5bfd45aaff72ecd074d060586942b7d4563c0d Mon Sep 17 00:00:00 2001 From: Diana Huang Date: Wed, 12 Nov 2014 09:17:56 -0500 Subject: [PATCH 8/8] Fix pylint violation. --- lms/djangoapps/shoppingcart/processors/CyberSource2.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource2.py b/lms/djangoapps/shoppingcart/processors/CyberSource2.py index 1d66138a96..68e35247c0 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource2.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource2.py @@ -367,6 +367,7 @@ 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: