diff --git a/cms/djangoapps/contentstore/management/commands/export_convert_format.py b/cms/djangoapps/contentstore/management/commands/export_convert_format.py index 5b1b1d7cfdb21c2444328bad14b21e027afa3c8f..f97ff305fc76ab36a51bdf5de336b58881f41eb2 100644 --- a/cms/djangoapps/contentstore/management/commands/export_convert_format.py +++ b/cms/djangoapps/contentstore/management/commands/export_convert_format.py @@ -7,6 +7,7 @@ Sample invocation: ./manage.py export_convert_format mycourse.tar.gz ~/newformat import os from path import path from django.core.management.base import BaseCommand, CommandError +from django.conf import settings from tempfile import mkdtemp import tarfile @@ -32,8 +33,8 @@ class Command(BaseCommand): output_path = args[1] # Create temp directories to extract the source and create the target archive. - temp_source_dir = mkdtemp() - temp_target_dir = mkdtemp() + temp_source_dir = mkdtemp(dir=settings.DATA_DIR) + temp_target_dir = mkdtemp(dir=settings.DATA_DIR) try: extract_source(source_archive, temp_source_dir) diff --git a/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py b/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py index fd83d58f890bb44c925965353b01dee390acc661..ddcdb725fbede91c504b91efac51016f5dc4fec6 100644 --- a/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py +++ b/cms/djangoapps/contentstore/management/commands/tests/test_export_convert_format.py @@ -3,6 +3,7 @@ Test for export_convert_format. """ from unittest import TestCase from django.core.management import call_command, CommandError +from django.conf import settings from tempfile import mkdtemp import shutil from path import path @@ -18,7 +19,7 @@ class ConvertExportFormat(TestCase): """ Common setup. """ super(ConvertExportFormat, self).setUp() - self.temp_dir = mkdtemp() + self.temp_dir = mkdtemp(dir=settings.DATA_DIR) self.addCleanup(shutil.rmtree, self.temp_dir) self.data_dir = path(__file__).realpath().parent / 'data' self.version0 = self.data_dir / "Version0_drafts.tar.gz" @@ -52,8 +53,8 @@ class ConvertExportFormat(TestCase): """ Helper function for determining if 2 archives are equal. """ - temp_dir_1 = mkdtemp() - temp_dir_2 = mkdtemp() + temp_dir_1 = mkdtemp(dir=settings.DATA_DIR) + temp_dir_2 = mkdtemp(dir=settings.DATA_DIR) try: extract_source(file1, temp_dir_1) extract_source(file2, temp_dir_2) diff --git a/cms/djangoapps/contentstore/views/tests/test_import_export.py b/cms/djangoapps/contentstore/views/tests/test_import_export.py index 3375a30d0916c1c5523e5c93e2efee879d404f77..f251d0a295979ed91e3544eaf2a52450a8464580 100644 --- a/cms/djangoapps/contentstore/views/tests/test_import_export.py +++ b/cms/djangoapps/contentstore/views/tests/test_import_export.py @@ -209,6 +209,19 @@ class ImportTestCase(CourseTestCase): return outside_tar + def _edx_platform_tar(self): + """ + Tarfile with file that extracts to edx-platform directory. + + Extracting this tarfile in directory <dir> will also put its contents + directly in <dir> (rather than <dir/tarname>). + """ + outside_tar = self.unsafe_common_dir / "unsafe_file.tar.gz" + with tarfile.open(outside_tar, "w:gz") as tar: + tar.addfile(tarfile.TarInfo(os.path.join(os.path.abspath("."), "a_file"))) + + return outside_tar + def test_unsafe_tar(self): """ Check that safety measure work. @@ -233,6 +246,12 @@ class ImportTestCase(CourseTestCase): try_tar(self._symlink_tar()) try_tar(self._outside_tar()) try_tar(self._outside_tar2()) + try_tar(self._edx_platform_tar()) + + # test trying to open a tar outside of the normal data directory + with self.settings(DATA_DIR='/not/the/data/dir'): + try_tar(self._edx_platform_tar()) + # Check that `import_status` returns the appropriate stage (i.e., # either 3, indicating all previous steps are completed, or 0, # indicating no upload in progress) @@ -294,13 +313,19 @@ class ImportTestCase(CourseTestCase): self.assertIn(test_block3.url_name, children) self.assertIn(test_block4.url_name, children) - extract_dir = path(tempfile.mkdtemp()) + extract_dir = path(tempfile.mkdtemp(dir=settings.DATA_DIR)) + # the extract_dir needs to be passed as a relative dir to + # import_library_from_xml + extract_dir_relative = path.relpath(extract_dir, settings.DATA_DIR) + try: - tar = tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz') - safetar_extractall(tar, extract_dir) + with tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz') as tar: + safetar_extractall(tar, extract_dir) library_items = import_library_from_xml( - self.store, self.user.id, - settings.GITHUB_REPO_ROOT, [extract_dir / 'library'], + self.store, + self.user.id, + settings.GITHUB_REPO_ROOT, + [extract_dir_relative / 'library'], load_error_modules=False, static_content_store=contentstore(), target_id=lib_key diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index 8c7e7f8585eeb4ec16574b12a5bebdb85e4bf650..84916debe96b6fba8f63dbf51f047e62708deeb5 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -39,6 +39,7 @@ INSTALLED_APPS += ('django_extensions',) TEST_ROOT = REPO_ROOT / "test_root" # pylint: disable=no-value-for-parameter GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath() LOG_DIR = (TEST_ROOT / "log").abspath() +DATA_DIR = TEST_ROOT / "data" # Configure modulestore to use the test folder within the repo update_module_store_settings( diff --git a/cms/envs/test.py b/cms/envs/test.py index ba6d9e7df1202e8a5e502a497171ba0db40c209a..289c3c67d378bc82cfa0f24f81b3cf6819cb39c1 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -65,6 +65,7 @@ TEST_ROOT = path('test_root') STATIC_ROOT = TEST_ROOT / "staticfiles" GITHUB_REPO_ROOT = TEST_ROOT / "data" +DATA_DIR = TEST_ROOT / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" # For testing "push to lms" diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 3883a5acd819e1fe8b5929cb23951a12f28b9522..11d2afc3944f0dcc70723af1b74e6c38c27b9696 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -325,6 +325,22 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): self.assertEquals(course_modes, expected_modes) + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + @patch.dict(settings.FEATURES, {"IS_EDX_DOMAIN": True}) + def test_hide_nav(self): + # Create the course modes + for mode in ["honor", "verified"]: + CourseModeFactory(mode_slug=mode, course_id=self.course.id) + + # Load the track selection page + url = reverse('course_modes_choose', args=[unicode(self.course.id)]) + response = self.client.get(url) + + # Verify that the header navigation links are hidden for the edx.org version + self.assertNotContains(response, "How it Works") + self.assertNotContains(response, "Find courses") + self.assertNotContains(response, "Schools & Partners") + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 290aef92495e1f8b4d753894900833b63bb28103..0dc790b7667b29261c05233d48be7631f3766ec1 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -119,7 +119,8 @@ class ChooseModeView(View): "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, - "responsive": True + "responsive": True, + "nav_hidden": True, } if "verified" in modes: context["suggested_prices"] = [ diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py index 5051421cd40ebddc72c0ab4ef5556afdd9209e98..00af8014b5f11e838b13745c8313af1319be53e1 100644 --- a/common/djangoapps/student/tests/tests.py +++ b/common/djangoapps/student/tests/tests.py @@ -529,6 +529,19 @@ class DashboardTest(ModuleStoreTestCase): response_3 = self.client.get(reverse('dashboard')) self.assertEquals(response_3.status_code, 200) + @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') + @patch.dict(settings.FEATURES, {"IS_EDX_DOMAIN": True}) + def test_dashboard_header_nav_has_find_courses(self): + self.client.login(username="jack", password="test") + response = self.client.get(reverse("dashboard")) + + # "Find courses" is shown in the side panel + self.assertContains(response, "Find courses") + + # But other links are hidden in the navigation + self.assertNotContains(response, "How it Works") + self.assertNotContains(response, "Schools & Partners") + class UserSettingsEventTestMixin(EventTestMixin): """ diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 03dd7ef302d0857259d715214828297916e58ce4..60c1f088095423247ae5740c8c18097cf68fd134 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -694,6 +694,7 @@ def dashboard(request): 'order_history_list': order_history_list, 'courses_requirements_not_met': courses_requirements_not_met, 'ccx_membership_triplets': ccx_membership_triplets, + 'nav_hidden': True, } return render_to_response('dashboard.html', context) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index e12b1454f4459ccb13f4c9097783391315081014..2f55f57a4147b15c94f18e9b4653a2cfbcc61fe3 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -591,6 +591,12 @@ class XModuleMixin(XModuleFields, XBlock): if field.scope.user == UserScope.ONE: field._del_cached_value(self) # pylint: disable=protected-access + # not the most elegant way of doing this, but if we're removing + # a field from the module's field_data_cache, we should also + # remove it from its _dirty_fields + # pylint: disable=protected-access + if field in self._dirty_fields: + del self._dirty_fields[field] # Set the new xmodule_runtime and field_data (which are user-specific) self.xmodule_runtime = xmodule_runtime diff --git a/common/static/js/utils/rwd_header.js b/common/static/js/utils/rwd_header.js deleted file mode 100644 index d2137fd9bcb8f8a996f2b29613dc85e581e1c179..0000000000000000000000000000000000000000 --- a/common/static/js/utils/rwd_header.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Adds rwd classes and click handlers. - */ - -(function($) { - 'use strict'; - - var rwd = (function() { - - var _fn = { - header: 'header.global-new', - - resultsUrl: 'course-search', - - init: function() { - _fn.$header = $( _fn.header ); - _fn.$footer = $( _fn.footer ); - _fn.$navContainer = _fn.$header.find('.nav-container'); - _fn.$globalNav = _fn.$header.find('.nav-global'); - - _fn.add.elements(); - _fn.add.classes(); - _fn.eventHandlers.init(); - }, - - add: { - classes: function() { - // Add any RWD-specific classes - _fn.$header.addClass('rwd'); - }, - - elements: function() { - _fn.add.burger(); - _fn.add.registerLink(); - }, - - burger: function() { - _fn.$navContainer.prepend([ - '<a href="#" class="mobile-menu-button" aria-label="menu">', - '<i class="icon fa fa-bars" aria-hidden="true"></i>', - '</a>' - ].join('')); - }, - - registerLink: function() { - var $register = _fn.$header.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/>'); - $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.$header.on( 'click', '.mobile-menu-button', _fn.toggleMenu ); - } - }, - - toggleMenu: function( event ) { - event.preventDefault(); - - _fn.$globalNav.toggleClass('show'); - } - }; - - return { - init: _fn.init - }; - })(); - - rwd.init(); -})(jQuery); diff --git a/common/test/acceptance/pages/lms/dashboard.py b/common/test/acceptance/pages/lms/dashboard.py index 30b72a4e37cae2260664b9c333ca6cb006596a42..cba42d2154647bab50531110e455d0bbdee7b988 100644 --- a/common/test/acceptance/pages/lms/dashboard.py +++ b/common/test/acceptance/pages/lms/dashboard.py @@ -48,20 +48,6 @@ class DashboardPage(PageObject): return self.q(css='h3.course-title > a').map(_get_course_name).results - @property - def sidebar_menu_title(self): - """ - Return the title value for sidebar menu. - """ - return self.q(css='.user-info span.title').text[0] - - @property - def sidebar_menu_description(self): - """ - Return the description text for sidebar menu. - """ - return self.q(css='.user-info span.copy').text[0] - def get_enrollment_mode(self, course_name): """Get the enrollment mode for a given course on the dashboard. diff --git a/common/test/acceptance/tests/lms/test_lms.py b/common/test/acceptance/tests/lms/test_lms.py index aca838c47bce640cf496d5137f874832c72c3ea3..9d0ae381c259f9daad536bdf2c596c1550d105b1 100644 --- a/common/test/acceptance/tests/lms/test_lms.py +++ b/common/test/acceptance/tests/lms/test_lms.py @@ -267,12 +267,6 @@ class RegisterFromCombinedPageTest(UniqueCourseTest): course_names = self.dashboard_page.wait_for_page().available_courses self.assertIn(self.course_info["display_name"], course_names) - self.assertEqual("want to change your account settings?", self.dashboard_page.sidebar_menu_title.lower()) - self.assertEqual( - "click the arrow next to your username above.", - self.dashboard_page.sidebar_menu_description.lower() - ) - def test_register_failure(self): # Navigate to the registration page self.register_page.visit() diff --git a/lms/djangoapps/commerce/tests/test_views.py b/lms/djangoapps/commerce/tests/test_views.py index f04b530debc67176d20b11e983cdddd3aa90ec18..80c4920edbdb34c587d640628fab81f35ebe6c00 100644 --- a/lms/djangoapps/commerce/tests/test_views.py +++ b/lms/djangoapps/commerce/tests/test_views.py @@ -84,3 +84,14 @@ class ReceiptViewTests(UserMixin, TestCase): system_message = "A system error occurred while processing your payment" self.assertRegexpMatches(response.content, user_message if is_user_message_expected else system_message) self.assertNotRegexpMatches(response.content, user_message if not is_user_message_expected else system_message) + + @mock.patch.dict(settings.FEATURES, {"IS_EDX_DOMAIN": True}) + def test_hide_nav_header(self): + self._login() + post_data = {'decision': 'ACCEPT', 'reason_code': '200', 'signed_field_names': 'dummy'} + response = self.post_to_receipt_page(post_data) + + # Verify that the header navigation links are hidden for the edx.org version + self.assertNotContains(response, "How it Works") + self.assertNotContains(response, "Find courses") + self.assertNotContains(response, "Schools & Partners") diff --git a/lms/djangoapps/commerce/views.py b/lms/djangoapps/commerce/views.py index 9c504878332b0642897d487dbfee4fe7d18991ed..58e1ec0cd89f95df92c7982ea3af168842209cf7 100644 --- a/lms/djangoapps/commerce/views.py +++ b/lms/djangoapps/commerce/views.py @@ -66,6 +66,7 @@ def checkout_receipt(request): 'error_text': error_text, 'for_help_text': for_help_text, 'payment_support_email': payment_support_email, - 'username': request.user.username + 'username': request.user.username, + 'nav_hidden': True, } return render_to_response('commerce/checkout_receipt.html', context) diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index d5c9824cad3ca3a23cccfbb8c0b0c4baa7e64173..d9f1ebf98f826082adcc921a981ac84cc148b012 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -1360,23 +1360,44 @@ class TestRebindModule(TestSubmittingProblems): super(TestRebindModule, self).setUp() self.homework = self.add_graded_section_to_course('homework') self.lti = ItemFactory.create(category='lti', parent=self.homework) + self.problem = ItemFactory.create(category='problem', parent=self.homework) self.user = UserFactory.create() self.anon_user = AnonymousUser() - def get_module_for_user(self, user): + def get_module_for_user(self, user, item=None): """Helper function to get useful module at self.location in self.course_id for user""" mock_request = MagicMock() mock_request.user = user field_data_cache = FieldDataCache.cache_for_descriptor_descendents( self.course.id, user, self.course, depth=2) + if item is None: + item = self.lti + return render.get_module( # pylint: disable=protected-access user, mock_request, - self.lti.location, + item.location, field_data_cache, )._xmodule + def test_rebind_module_to_new_users(self): + module = self.get_module_for_user(self.user, self.problem) + + # Bind the module to another student, which will remove "correct_map" + # from the module's _field_data_cache and _dirty_fields. + user2 = UserFactory.create() + module.descriptor.bind_for_student(module.system, user2.id) + + # XBlock's save method assumes that if a field is in _dirty_fields, + # then it's also in _field_data_cache. If this assumption + # doesn't hold, then we get an error trying to bind this module + # to a third student, since we've removed "correct_map" from + # _field_data cache, but not _dirty_fields, when we bound + # this module to the second student. (TNL-2640) + user3 = UserFactory.create() + module.descriptor.bind_for_student(module.system, user3.id) + def test_rebind_noauth_module_to_user_not_anonymous(self): """ Tests that an exception is thrown when rebind_noauth_module_to_user is run from a diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 0d900044a9006c6008a97c7f9f381383580cf045..60ebc36c861bd54783be78fc746a1ab2142bde51 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -237,6 +237,17 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): ) self._assert_redirects_to_dashboard(response) + @patch.dict(settings.FEATURES, {"IS_EDX_DOMAIN": True}) + def test_pay_and_verify_hides_header_nav(self): + course = self._create_course("verified") + self._enroll(course.id, "verified") + response = self._get_page('verify_student_start_flow', course.id) + + # Verify that the header navigation links are hidden for the edx.org version + self.assertNotContains(response, "How it Works") + self.assertNotContains(response, "Find courses") + self.assertNotContains(response, "Schools & Partners") + def test_verify_now(self): # We've already paid, and now we're trying to verify course = self._create_course("verified") diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py index 288a86710b110370e25d392a3e98e242ece66430..74d993ebb2573210048f338903b41a8f5e183a03 100644 --- a/lms/djangoapps/verify_student/views.py +++ b/lms/djangoapps/verify_student/views.py @@ -376,6 +376,7 @@ class PayAndVerifyView(View): 'already_verified': already_verified, 'verification_good_until': verification_good_until, 'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"), + 'nav_hidden': True, } return render_to_response("verify_student/pay_and_verify.html", context) diff --git a/lms/envs/aws.py b/lms/envs/aws.py index 94e7563d2a6c392609cc065091ef710e01eca2b6..4952abed45b8949012c5940faa4071ef99e6eeaf 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -678,3 +678,8 @@ if FEATURES.get('ENABLE_LTI_PROVIDER'): ##################### Credit Provider help link #################### CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_URL) + + +#### JWT configuration #### +JWT_ISSUER = ENV_TOKENS.get('JWT_ISSUER', JWT_ISSUER) +JWT_EXPIRATION = ENV_TOKENS.get('JWT_EXPIRATION', JWT_EXPIRATION) diff --git a/lms/envs/common.py b/lms/envs/common.py index 833c8c6d61654389a30c7b079b24967c0ad929cf..1cd367d770fde02c4a6c5815ec9583e6f2c20457 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1247,7 +1247,6 @@ dashboard_js = ( ) dashboard_search_js = ['js/search/dashboard/main.js'] discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.js')) -rwd_header_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'js/utils/rwd_header.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')) @@ -1260,7 +1259,6 @@ instructor_dash_js = ( # These are not courseware, so they do not need many of the courseware-specific # JavaScript modules. student_account_js = [ - 'js/utils/rwd_header.js', 'js/utils/edx.utils.validate.js', 'js/form.ext.js', 'js/my_courses_dropdown.js', @@ -1549,10 +1547,6 @@ PIPELINE_JS = { 'source_filenames': dashboard_search_js, 'output_filename': 'js/dashboard-search.js', }, - 'rwd_header': { - 'source_filenames': rwd_header_js, - 'output_filename': 'js/rwd_header.js' - }, 'student_account': { 'source_filenames': student_account_js, 'output_filename': 'js/student_account.js' diff --git a/lms/static/sass/elements/_controls.scss b/lms/static/sass/elements/_controls.scss index ceede5d14f081b0d7f1d4d90368145257f457364..a38dc4a7399866216343df792700cdf7a3884dc2 100644 --- a/lms/static/sass/elements/_controls.scss +++ b/lms/static/sass/elements/_controls.scss @@ -352,6 +352,19 @@ } } +%btn-pl-elevated-alt { + @extend %btn-pl-default-base; + box-shadow: 0 3px 0 0 $gray-l4; + border: 1px solid $gray-l4; + + &:hover { + box-shadow: 0 3px 0 0 $action-primary-bg; + border: 1px solid $action-primary-bg; + background-color: lighten($action-primary-bg,20%); + color: $white; + } +} + // ==================== // application: canned actions diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss index 1ecf983515cfb8446bb11134f4d5a57b4f2f09ee..64f10ca9a20f6667eb68b394314ed52dbcaf70d5 100644 --- a/lms/static/sass/multicourse/_dashboard.scss +++ b/lms/static/sass/multicourse/_dashboard.scss @@ -15,14 +15,31 @@ @include clearfix(); padding: ($baseline*2) 0 $baseline 0; + .wrapper-find-courses { + @include float(right); + @include margin-left(flex-gutter()); + width: flex-grid(3); + margin-top: ($baseline*2); + border-top: 3px solid $blue; + padding: $baseline 0; + + .copy { + @extend %t-copy-sub1; + } + + .btn-find-courses { + @extend %btn-pl-elevated-alt; + } + } + .profile-sidebar { background: transparent; @include float(right); - margin-top: ($baseline*2); + @include margin-left(flex-gutter()); width: flex-grid(3); - box-shadow: 0 0 1px $shadow-l1; - border: 1px solid $border-color-2; - border-radius: 3px; + margin-top: ($baseline*2); + border-top: 3px solid $blue; + padding: $baseline 0; .user-info { @include clearfix(); @@ -31,7 +48,7 @@ @include box-sizing(border-box); @include clearfix(); margin: 0; - padding: $baseline; + padding: 0; width: flex-grid(12); li { @@ -59,7 +76,7 @@ } span.title { - @extend %t-copy-sub1; + @extend %t-title6; @extend %t-strong; a { diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 648b9404162a2ab80ff85a23c4f76529ccc5b9cd..43535f2ddd805335faf848b1853b357627b333a1 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -620,8 +620,8 @@ header.global-new { a { display:block; padding: 3px 10px; - font-size: 18px; - line-height: 24px; + font-size: 14px; + line-height: 1.5; font-weight: 600; font-family: $header-sans-serif; color: $courseware-navigation-color; @@ -708,7 +708,6 @@ header.global-new { font-size: 14px; &.nav-courseware-button { - width: 86px; text-align: center; margin-top: -3px; } @@ -826,13 +825,6 @@ header.global-new { .wrapper-header { padding: 17px 0; } - - .nav-global, - .nav-courseware { - a { - font-size: 18px; - } - } } } } diff --git a/lms/templates/commerce/checkout_receipt.html b/lms/templates/commerce/checkout_receipt.html index 12ea45a9de313e143891e35bf37db3c3a77e4eba..3ff3b73bc7d41b0a1557af86a34026732c1a17e6 100644 --- a/lms/templates/commerce/checkout_receipt.html +++ b/lms/templates/commerce/checkout_receipt.html @@ -18,7 +18,6 @@ from django.utils.translation import ugettext as _ </%block> <%block name="js_extra"> -<%static:js group='rwd_header'/> <script src="${static.url('js/vendor/jquery.ajax-retry.js')}"></script> <script src="${static.url('js/vendor/underscore-min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script> diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 8d3fb0ad4388a208544e4bf305150101a146cd0f..5d1ffc4c8875c6ac4df83f6718262e29af9b0fca 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -147,16 +147,19 @@ from django.core.urlresolvers import reverse <section id="dashboard-search-results" class="search-results dashboard-search-results"></section> % endif - <section class="profile-sidebar" id="profile-sidebar" role="region" aria-label="User info"> + % if settings.FEATURES.get('IS_EDX_DOMAIN'): + <div class="wrapper-find-courses"> + <p class="copy">Check out our recently launched courses and what's new in your favorite subjects</p> + <p><a class="btn-find-courses" href="${marketing_link('COURSES')}">${_("Find New Courses")}</a></p> + </div> + % endif + + <section class="profile-sidebar" id="profile-sidebar" role="region" aria-label="Account Status Info"> <header class="profile"> - <h2 class="username-header"><span class="sr">${_("Username")}: </span></h2> + <h2 class="account-status-title sr">${_("Account Status Info")}: </h2> </header> <section class="user-info"> <ul> - <li class="heads-up"> - <span class="title">${_("Want to change your account settings?")}</span> - <span class="copy">${_("Click the arrow next to your username above.")}</span> - </li> % if len(order_history_list): <li class="order-history"> diff --git a/lms/templates/navigation-edx.html b/lms/templates/navigation-edx.html index 8374bb6e139ee4b529391c5a29b3bf73bef090d9..d0c17fb22bf84f050b9dabf02a6db54356fc25b1 100644 --- a/lms/templates/navigation-edx.html +++ b/lms/templates/navigation-edx.html @@ -53,6 +53,7 @@ site_status_msg = get_site_status_msg(course_id) % if user.is_authenticated(): % if not course or disable_courseware_header: + % if not nav_hidden: <nav aria-label="Main" class="nav-main"> <ul class="left nav-global authenticated"> <%block name="navigation_global_links_authenticated"> @@ -68,6 +69,7 @@ site_status_msg = get_site_status_msg(course_id) </%block> </ul> </nav> + % endif % endif <ul class="user"> @@ -101,44 +103,28 @@ site_status_msg = get_site_status_msg(course_id) % endif % else: - <nav aria-label="Main" class="nav-main"> - <ul class="left nav-global"> - <%block name="navigation_global_links"> - <li class="nav-global-01"> - <a href="${marketing_link('HOW_IT_WORKS')}">${_("How it Works")}</a> - </li> - <li class="nav-global-02"> - <a href="${marketing_link('COURSES')}">${_("Find Courses")}</a> - </li> - <li class="nav-global-03"> - <a href="${marketing_link('SCHOOLS')}">${_("Schools & Partners")}</a> - </li> - </%block> - </ul> - </nav> - <nav aria-label="Account" class="nav-account-management"> <div class="right nav-courseware"> + <div class="nav-courseware-01"> + % if not settings.FEATURES['DISABLE_LOGIN_BUTTON']: + % if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: + <a class="cta cta-login" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a> + % else: + <a class="cta cta-login" href="/login${login_query()}">${_("Sign in")}</a> + % endif + % endif + </div> % if not settings.FEATURES['DISABLE_LOGIN_BUTTON']: % if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: - <div class="nav-courseware-01"> - <a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a> + <div class="nav-courseware-02"> + <a class="cta cta-register nav-courseware-button" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a> </div> % else: - <div class="nav-courseware-01"> - <a class="cta cta-register" href="/register">${_("Register")}</a> + <div class="nav-courseware-02"> + <a class="cta cta-register nav-courseware-button" href="/register">${_("Register")}</a> </div> % endif % endif - <div class="nav-courseware-02"> - % if not settings.FEATURES['DISABLE_LOGIN_BUTTON']: - % if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: - <a class="cta cta-login nav-courseware-button" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a> - % else: - <a class="cta cta-login nav-courseware-button" href="/login${login_query()}">${_("Sign in")}</a> - % endif - % endif - </div> </div> </nav> % endif diff --git a/lms/templates/verify_student/incourse_reverify.html b/lms/templates/verify_student/incourse_reverify.html index a6a3144bc5e673cd024eaf2129970fd17c172abd..a7b101dff4c7f6c96d7606d1889a8dbdcdb25527 100644 --- a/lms/templates/verify_student/incourse_reverify.html +++ b/lms/templates/verify_student/incourse_reverify.html @@ -18,7 +18,6 @@ from django.utils.translation import ugettext as _ % endfor </%block> <%block name="js_extra"> - <%static:js group='rwd_header'/> <script src="${static.url('js/vendor/underscore-min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script> diff --git a/lms/templates/verify_student/pay_and_verify.html b/lms/templates/verify_student/pay_and_verify.html index 802ca32ac0662cdf6953c127ae34883f0f6bf182..7b90d72001c2f43ca01cde7d17f378123386f5cd 100644 --- a/lms/templates/verify_student/pay_and_verify.html +++ b/lms/templates/verify_student/pay_and_verify.html @@ -35,7 +35,6 @@ from verify_student.views import PayAndVerifyView % endfor </%block> <%block name="js_extra"> - <%static:js group='rwd_header'/> <script src="${static.url('js/vendor/underscore-min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script> diff --git a/lms/templates/verify_student/reverify.html b/lms/templates/verify_student/reverify.html index 3f2d0db042d5f3306d98a24461d454bb84f32927..8218392e9571707fb6caff949cdbcd4deca9bf95 100644 --- a/lms/templates/verify_student/reverify.html +++ b/lms/templates/verify_student/reverify.html @@ -16,7 +16,6 @@ from django.utils.translation import ugettext as _ % endfor </%block> <%block name="js_extra"> - <%static:js group='rwd_header'/> <script src="${static.url('js/vendor/underscore-min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script> diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0004_default_lowest_passing_grade_to_None.py b/openedx/core/djangoapps/content/course_overviews/migrations/0004_default_lowest_passing_grade_to_None.py new file mode 100644 index 0000000000000000000000000000000000000000..0876dbe7470e22e3d830f5739d9028b7af9a9a34 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0004_default_lowest_passing_grade_to_None.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'CourseOverview.lowest_passing_grade' + db.alter_column('course_overviews_courseoverview', 'lowest_passing_grade', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=5, decimal_places=2)) + + def backwards(self, orm): + + # Changing field 'CourseOverview.lowest_passing_grade' + db.alter_column('course_overviews_courseoverview', 'lowest_passing_grade', self.gf('django.db.models.fields.DecimalField')(default=0.5, max_digits=5, decimal_places=2)) + + models = { + 'course_overviews.courseoverview': { + 'Meta': {'object_name': 'CourseOverview'}, + '_location': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255'}), + '_pre_requisite_courses_json': ('django.db.models.fields.TextField', [], {}), + 'advertised_start': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'cert_html_view_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'cert_name_long': ('django.db.models.fields.TextField', [], {}), + 'cert_name_short': ('django.db.models.fields.TextField', [], {}), + 'certificates_display_behavior': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'certificates_show_before_end': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'course_image_url': ('django.db.models.fields.TextField', [], {}), + 'days_early_for_beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}), + 'display_name': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'display_number_with_default': ('django.db.models.fields.TextField', [], {}), + 'display_org_with_default': ('django.db.models.fields.TextField', [], {}), + 'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'end_of_course_survey_url': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'facebook_url': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'has_any_active_web_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'primary_key': 'True', 'db_index': 'True'}), + 'lowest_passing_grade': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '5', 'decimal_places': '2'}), + 'mobile_available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'social_sharing_url': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}), + 'visible_to_staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}) + } + } + + complete_apps = ['course_overviews'] \ No newline at end of file diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 14387a8d96c716fbe7661dbd3772188a4fbd8d27..4b8429f9cc7183d75a9ec4dffc7e4ba1f4aaca0e 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -52,7 +52,7 @@ class CourseOverview(django.db.models.Model): cert_name_long = TextField() # Grading - lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2) + lowest_passing_grade = DecimalField(max_digits=5, decimal_places=2, null=True) # Access parameters days_early_for_beta = FloatField(null=True) @@ -77,6 +77,16 @@ class CourseOverview(django.db.models.Model): from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.courses import course_image_url + # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. + # If the course has a malformed grading policy such that + # course._grading_policy['GRADE_CUTOFFS'] = {}, then + # course.lowest_passing_grade will raise a ValueError. + # Work around this for now by defaulting to None. + try: + lowest_passing_grade = course.lowest_passing_grade + except ValueError: + lowest_passing_grade = None + return CourseOverview( id=course.id, _location=course.location, @@ -98,7 +108,7 @@ class CourseOverview(django.db.models.Model): has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, - lowest_passing_grade=course.lowest_passing_grade, + lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, diff --git a/openedx/core/djangoapps/content/course_overviews/tests.py b/openedx/core/djangoapps/content/course_overviews/tests.py index dc87856ae64ec14f30603e0694f836393c069100..8a56bb51725948be2e6e422215b1d96f6d74cde4 100644 --- a/openedx/core/djangoapps/content/course_overviews/tests.py +++ b/openedx/core/djangoapps/content/course_overviews/tests.py @@ -294,3 +294,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase): # which causes get_from_id to raise an IOError. with self.assertRaises(IOError): CourseOverview.get_from_id(course.id) + + def test_malformed_grading_policy(self): + """ + Test that CourseOverview handles courses with a malformed grading policy + such that course._grading_policy['GRADE_CUTOFFS'] = {} by defaulting + .lowest_passing_grade to None. + + Created in response to https://openedx.atlassian.net/browse/TNL-2806. + """ + course = CourseFactory.create() + course._grading_policy['GRADE_CUTOFFS'] = {} # pylint: disable=protected-access + with self.assertRaises(ValueError): + __ = course.lowest_passing_grade + course_overview = CourseOverview._create_from_course(course) # pylint: disable=protected-access + self.assertEqual(course_overview.lowest_passing_grade, None) diff --git a/openedx/core/lib/extract_tar.py b/openedx/core/lib/extract_tar.py index ea464880ea18f051e4fbf05323a763d723b816be..58079ad065a22dd866fea6f9873d70725ccf92be 100644 --- a/openedx/core/lib/extract_tar.py +++ b/openedx/core/lib/extract_tar.py @@ -7,6 +7,7 @@ http://stackoverflow.com/questions/10060069/safely-extract-zip-or-tar-using-pyth """ from os.path import abspath, realpath, dirname, join as joinpath from django.core.exceptions import SuspiciousOperation +from django.conf import settings import logging log = logging.getLogger(__name__) @@ -28,19 +29,23 @@ def _is_bad_path(path, base): def _is_bad_link(info, base): """ - Does the file sym- ord hard-link to files outside `base`? + Does the file sym- or hard-link to files outside `base`? """ # Links are interpreted relative to the directory containing the link tip = resolved(joinpath(base, dirname(info.name))) return _is_bad_path(info.linkname, base=tip) -def safemembers(members): +def safemembers(members, base): """ Check that all elements of a tar file are safe. """ - base = resolved(".") + base = resolved(base) + + # check that we're not trying to import outside of the data_dir + if not base.startswith(resolved(settings.DATA_DIR)): + raise SuspiciousOperation("Attempted to import course outside of data dir") for finfo in members: if _is_bad_path(finfo.name, base): @@ -61,8 +66,8 @@ def safemembers(members): return members -def safetar_extractall(tarf, *args, **kwargs): +def safetar_extractall(tar_file, path=".", members=None): # pylint: disable=unused-argument """ - Safe version of `tarf.extractall()`. + Safe version of `tar_file.extractall()`. """ - return tarf.extractall(members=safemembers(tarf), *args, **kwargs) + return tar_file.extractall(path, safemembers(tar_file, path))