Commit 80cf4d6e by Max Rothman

Merge pull request #8969 from edx/release-merge-test

Release merge test
parents 4c0b00f4 babf2420
...@@ -7,6 +7,7 @@ Sample invocation: ./manage.py export_convert_format mycourse.tar.gz ~/newformat ...@@ -7,6 +7,7 @@ Sample invocation: ./manage.py export_convert_format mycourse.tar.gz ~/newformat
import os import os
from path import path from path import path
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
from tempfile import mkdtemp from tempfile import mkdtemp
import tarfile import tarfile
...@@ -32,8 +33,8 @@ class Command(BaseCommand): ...@@ -32,8 +33,8 @@ class Command(BaseCommand):
output_path = args[1] output_path = args[1]
# Create temp directories to extract the source and create the target archive. # Create temp directories to extract the source and create the target archive.
temp_source_dir = mkdtemp() temp_source_dir = mkdtemp(dir=settings.DATA_DIR)
temp_target_dir = mkdtemp() temp_target_dir = mkdtemp(dir=settings.DATA_DIR)
try: try:
extract_source(source_archive, temp_source_dir) extract_source(source_archive, temp_source_dir)
......
...@@ -3,6 +3,7 @@ Test for export_convert_format. ...@@ -3,6 +3,7 @@ Test for export_convert_format.
""" """
from unittest import TestCase from unittest import TestCase
from django.core.management import call_command, CommandError from django.core.management import call_command, CommandError
from django.conf import settings
from tempfile import mkdtemp from tempfile import mkdtemp
import shutil import shutil
from path import path from path import path
...@@ -18,7 +19,7 @@ class ConvertExportFormat(TestCase): ...@@ -18,7 +19,7 @@ class ConvertExportFormat(TestCase):
""" Common setup. """ """ Common setup. """
super(ConvertExportFormat, self).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.addCleanup(shutil.rmtree, self.temp_dir)
self.data_dir = path(__file__).realpath().parent / 'data' self.data_dir = path(__file__).realpath().parent / 'data'
self.version0 = self.data_dir / "Version0_drafts.tar.gz" self.version0 = self.data_dir / "Version0_drafts.tar.gz"
...@@ -52,8 +53,8 @@ class ConvertExportFormat(TestCase): ...@@ -52,8 +53,8 @@ class ConvertExportFormat(TestCase):
""" """
Helper function for determining if 2 archives are equal. Helper function for determining if 2 archives are equal.
""" """
temp_dir_1 = mkdtemp() temp_dir_1 = mkdtemp(dir=settings.DATA_DIR)
temp_dir_2 = mkdtemp() temp_dir_2 = mkdtemp(dir=settings.DATA_DIR)
try: try:
extract_source(file1, temp_dir_1) extract_source(file1, temp_dir_1)
extract_source(file2, temp_dir_2) extract_source(file2, temp_dir_2)
......
...@@ -209,6 +209,19 @@ class ImportTestCase(CourseTestCase): ...@@ -209,6 +209,19 @@ class ImportTestCase(CourseTestCase):
return outside_tar 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): def test_unsafe_tar(self):
""" """
Check that safety measure work. Check that safety measure work.
...@@ -233,6 +246,12 @@ class ImportTestCase(CourseTestCase): ...@@ -233,6 +246,12 @@ class ImportTestCase(CourseTestCase):
try_tar(self._symlink_tar()) try_tar(self._symlink_tar())
try_tar(self._outside_tar()) try_tar(self._outside_tar())
try_tar(self._outside_tar2()) 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., # Check that `import_status` returns the appropriate stage (i.e.,
# either 3, indicating all previous steps are completed, or 0, # either 3, indicating all previous steps are completed, or 0,
# indicating no upload in progress) # indicating no upload in progress)
...@@ -294,13 +313,19 @@ class ImportTestCase(CourseTestCase): ...@@ -294,13 +313,19 @@ class ImportTestCase(CourseTestCase):
self.assertIn(test_block3.url_name, children) self.assertIn(test_block3.url_name, children)
self.assertIn(test_block4.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: try:
tar = tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz') with tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz') as tar:
safetar_extractall(tar, extract_dir) safetar_extractall(tar, extract_dir)
library_items = import_library_from_xml( library_items = import_library_from_xml(
self.store, self.user.id, self.store,
settings.GITHUB_REPO_ROOT, [extract_dir / 'library'], self.user.id,
settings.GITHUB_REPO_ROOT,
[extract_dir_relative / 'library'],
load_error_modules=False, load_error_modules=False,
static_content_store=contentstore(), static_content_store=contentstore(),
target_id=lib_key target_id=lib_key
......
...@@ -39,6 +39,7 @@ INSTALLED_APPS += ('django_extensions',) ...@@ -39,6 +39,7 @@ INSTALLED_APPS += ('django_extensions',)
TEST_ROOT = REPO_ROOT / "test_root" # pylint: disable=no-value-for-parameter TEST_ROOT = REPO_ROOT / "test_root" # pylint: disable=no-value-for-parameter
GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath() GITHUB_REPO_ROOT = (TEST_ROOT / "data").abspath()
LOG_DIR = (TEST_ROOT / "log").abspath() LOG_DIR = (TEST_ROOT / "log").abspath()
DATA_DIR = TEST_ROOT / "data"
# Configure modulestore to use the test folder within the repo # Configure modulestore to use the test folder within the repo
update_module_store_settings( update_module_store_settings(
......
...@@ -65,6 +65,7 @@ TEST_ROOT = path('test_root') ...@@ -65,6 +65,7 @@ TEST_ROOT = path('test_root')
STATIC_ROOT = TEST_ROOT / "staticfiles" STATIC_ROOT = TEST_ROOT / "staticfiles"
GITHUB_REPO_ROOT = TEST_ROOT / "data" GITHUB_REPO_ROOT = TEST_ROOT / "data"
DATA_DIR = TEST_ROOT / "data"
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
# For testing "push to lms" # For testing "push to lms"
......
...@@ -325,6 +325,22 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase): ...@@ -325,6 +325,22 @@ class CourseModeViewTest(UrlResetMixin, ModuleStoreTestCase):
self.assertEquals(course_modes, expected_modes) 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') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase): class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
......
...@@ -119,7 +119,8 @@ class ChooseModeView(View): ...@@ -119,7 +119,8 @@ class ChooseModeView(View):
"course_num": course.display_number_with_default, "course_num": course.display_number_with_default,
"chosen_price": chosen_price, "chosen_price": chosen_price,
"error": error, "error": error,
"responsive": True "responsive": True,
"nav_hidden": True,
} }
if "verified" in modes: if "verified" in modes:
context["suggested_prices"] = [ context["suggested_prices"] = [
......
...@@ -529,6 +529,19 @@ class DashboardTest(ModuleStoreTestCase): ...@@ -529,6 +529,19 @@ class DashboardTest(ModuleStoreTestCase):
response_3 = self.client.get(reverse('dashboard')) response_3 = self.client.get(reverse('dashboard'))
self.assertEquals(response_3.status_code, 200) 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): class UserSettingsEventTestMixin(EventTestMixin):
""" """
......
...@@ -694,6 +694,7 @@ def dashboard(request): ...@@ -694,6 +694,7 @@ def dashboard(request):
'order_history_list': order_history_list, 'order_history_list': order_history_list,
'courses_requirements_not_met': courses_requirements_not_met, 'courses_requirements_not_met': courses_requirements_not_met,
'ccx_membership_triplets': ccx_membership_triplets, 'ccx_membership_triplets': ccx_membership_triplets,
'nav_hidden': True,
} }
return render_to_response('dashboard.html', context) return render_to_response('dashboard.html', context)
......
...@@ -591,6 +591,12 @@ class XModuleMixin(XModuleFields, XBlock): ...@@ -591,6 +591,12 @@ class XModuleMixin(XModuleFields, XBlock):
if field.scope.user == UserScope.ONE: if field.scope.user == UserScope.ONE:
field._del_cached_value(self) # pylint: disable=protected-access 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) # Set the new xmodule_runtime and field_data (which are user-specific)
self.xmodule_runtime = xmodule_runtime self.xmodule_runtime = xmodule_runtime
......
/**
* 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);
...@@ -48,20 +48,6 @@ class DashboardPage(PageObject): ...@@ -48,20 +48,6 @@ class DashboardPage(PageObject):
return self.q(css='h3.course-title > a').map(_get_course_name).results 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): def get_enrollment_mode(self, course_name):
"""Get the enrollment mode for a given course on the dashboard. """Get the enrollment mode for a given course on the dashboard.
......
...@@ -267,12 +267,6 @@ class RegisterFromCombinedPageTest(UniqueCourseTest): ...@@ -267,12 +267,6 @@ class RegisterFromCombinedPageTest(UniqueCourseTest):
course_names = self.dashboard_page.wait_for_page().available_courses course_names = self.dashboard_page.wait_for_page().available_courses
self.assertIn(self.course_info["display_name"], course_names) 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): def test_register_failure(self):
# Navigate to the registration page # Navigate to the registration page
self.register_page.visit() self.register_page.visit()
......
...@@ -4,6 +4,7 @@ from uuid import uuid4 ...@@ -4,6 +4,7 @@ from uuid import uuid4
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
import ddt import ddt
from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
import mock import mock
...@@ -84,3 +85,14 @@ class ReceiptViewTests(UserMixin, TestCase): ...@@ -84,3 +85,14 @@ class ReceiptViewTests(UserMixin, TestCase):
system_message = "A system error occurred while processing your payment" 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.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) 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")
...@@ -66,6 +66,7 @@ def checkout_receipt(request): ...@@ -66,6 +66,7 @@ def checkout_receipt(request):
'error_text': error_text, 'error_text': error_text,
'for_help_text': for_help_text, 'for_help_text': for_help_text,
'payment_support_email': payment_support_email, '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) return render_to_response('commerce/checkout_receipt.html', context)
...@@ -1360,23 +1360,44 @@ class TestRebindModule(TestSubmittingProblems): ...@@ -1360,23 +1360,44 @@ class TestRebindModule(TestSubmittingProblems):
super(TestRebindModule, self).setUp() super(TestRebindModule, self).setUp()
self.homework = self.add_graded_section_to_course('homework') self.homework = self.add_graded_section_to_course('homework')
self.lti = ItemFactory.create(category='lti', parent=self.homework) self.lti = ItemFactory.create(category='lti', parent=self.homework)
self.problem = ItemFactory.create(category='problem', parent=self.homework)
self.user = UserFactory.create() self.user = UserFactory.create()
self.anon_user = AnonymousUser() 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""" """Helper function to get useful module at self.location in self.course_id for user"""
mock_request = MagicMock() mock_request = MagicMock()
mock_request.user = user mock_request.user = user
field_data_cache = FieldDataCache.cache_for_descriptor_descendents( field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
self.course.id, user, self.course, depth=2) self.course.id, user, self.course, depth=2)
if item is None:
item = self.lti
return render.get_module( # pylint: disable=protected-access return render.get_module( # pylint: disable=protected-access
user, user,
mock_request, mock_request,
self.lti.location, item.location,
field_data_cache, field_data_cache,
)._xmodule )._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): 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 Tests that an exception is thrown when rebind_noauth_module_to_user is run from a
......
...@@ -237,6 +237,17 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase): ...@@ -237,6 +237,17 @@ class TestPayAndVerifyView(UrlResetMixin, ModuleStoreTestCase):
) )
self._assert_redirects_to_dashboard(response) 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): def test_verify_now(self):
# We've already paid, and now we're trying to verify # We've already paid, and now we're trying to verify
course = self._create_course("verified") course = self._create_course("verified")
......
...@@ -376,6 +376,7 @@ class PayAndVerifyView(View): ...@@ -376,6 +376,7 @@ class PayAndVerifyView(View):
'already_verified': already_verified, 'already_verified': already_verified,
'verification_good_until': verification_good_until, 'verification_good_until': verification_good_until,
'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"), 'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"),
'nav_hidden': True,
} }
return render_to_response("verify_student/pay_and_verify.html", context) return render_to_response("verify_student/pay_and_verify.html", context)
......
...@@ -678,3 +678,8 @@ if FEATURES.get('ENABLE_LTI_PROVIDER'): ...@@ -678,3 +678,8 @@ if FEATURES.get('ENABLE_LTI_PROVIDER'):
##################### Credit Provider help link #################### ##################### Credit Provider help link ####################
CREDIT_HELP_LINK_URL = ENV_TOKENS.get('CREDIT_HELP_LINK_URL', CREDIT_HELP_LINK_URL) 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)
...@@ -1247,7 +1247,6 @@ dashboard_js = ( ...@@ -1247,7 +1247,6 @@ dashboard_js = (
) )
dashboard_search_js = ['js/search/dashboard/main.js'] dashboard_search_js = ['js/search/dashboard/main.js']
discussion_js = sorted(rooted_glob(COMMON_ROOT / 'static', 'coffee/src/discussion/**/*.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')) 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')) 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')) notes_js = sorted(rooted_glob(PROJECT_ROOT / 'static', 'coffee/src/notes/**/*.js'))
...@@ -1260,7 +1259,6 @@ instructor_dash_js = ( ...@@ -1260,7 +1259,6 @@ instructor_dash_js = (
# These are not courseware, so they do not need many of the courseware-specific # These are not courseware, so they do not need many of the courseware-specific
# JavaScript modules. # JavaScript modules.
student_account_js = [ student_account_js = [
'js/utils/rwd_header.js',
'js/utils/edx.utils.validate.js', 'js/utils/edx.utils.validate.js',
'js/form.ext.js', 'js/form.ext.js',
'js/my_courses_dropdown.js', 'js/my_courses_dropdown.js',
...@@ -1549,10 +1547,6 @@ PIPELINE_JS = { ...@@ -1549,10 +1547,6 @@ PIPELINE_JS = {
'source_filenames': dashboard_search_js, 'source_filenames': dashboard_search_js,
'output_filename': 'js/dashboard-search.js', 'output_filename': 'js/dashboard-search.js',
}, },
'rwd_header': {
'source_filenames': rwd_header_js,
'output_filename': 'js/rwd_header.js'
},
'student_account': { 'student_account': {
'source_filenames': student_account_js, 'source_filenames': student_account_js,
'output_filename': 'js/student_account.js' 'output_filename': 'js/student_account.js'
......
...@@ -352,6 +352,19 @@ ...@@ -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 // application: canned actions
......
...@@ -15,14 +15,31 @@ ...@@ -15,14 +15,31 @@
@include clearfix(); @include clearfix();
padding: ($baseline*2) 0 $baseline 0; 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 { .profile-sidebar {
background: transparent; background: transparent;
@include float(right); @include float(right);
margin-top: ($baseline*2); @include margin-left(flex-gutter());
width: flex-grid(3); width: flex-grid(3);
box-shadow: 0 0 1px $shadow-l1; margin-top: ($baseline*2);
border: 1px solid $border-color-2; border-top: 3px solid $blue;
border-radius: 3px; padding: $baseline 0;
.user-info { .user-info {
@include clearfix(); @include clearfix();
...@@ -31,7 +48,7 @@ ...@@ -31,7 +48,7 @@
@include box-sizing(border-box); @include box-sizing(border-box);
@include clearfix(); @include clearfix();
margin: 0; margin: 0;
padding: $baseline; padding: 0;
width: flex-grid(12); width: flex-grid(12);
li { li {
...@@ -59,7 +76,7 @@ ...@@ -59,7 +76,7 @@
} }
span.title { span.title {
@extend %t-copy-sub1; @extend %t-title6;
@extend %t-strong; @extend %t-strong;
a { a {
......
...@@ -620,8 +620,8 @@ header.global-new { ...@@ -620,8 +620,8 @@ header.global-new {
a { a {
display:block; display:block;
padding: 3px 10px; padding: 3px 10px;
font-size: 18px; font-size: 14px;
line-height: 24px; line-height: 1.5;
font-weight: 600; font-weight: 600;
font-family: $header-sans-serif; font-family: $header-sans-serif;
color: $courseware-navigation-color; color: $courseware-navigation-color;
...@@ -708,7 +708,6 @@ header.global-new { ...@@ -708,7 +708,6 @@ header.global-new {
font-size: 14px; font-size: 14px;
&.nav-courseware-button { &.nav-courseware-button {
width: 86px;
text-align: center; text-align: center;
margin-top: -3px; margin-top: -3px;
} }
...@@ -826,13 +825,6 @@ header.global-new { ...@@ -826,13 +825,6 @@ header.global-new {
.wrapper-header { .wrapper-header {
padding: 17px 0; padding: 17px 0;
} }
.nav-global,
.nav-courseware {
a {
font-size: 18px;
}
}
} }
} }
} }
......
...@@ -18,7 +18,6 @@ from django.utils.translation import ugettext as _ ...@@ -18,7 +18,6 @@ from django.utils.translation import ugettext as _
</%block> </%block>
<%block name="js_extra"> <%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/jquery.ajax-retry.js')}"></script>
<script src="${static.url('js/vendor/underscore-min.js')}"></script> <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/underscore.string.min.js')}"></script>
......
...@@ -147,16 +147,19 @@ from django.core.urlresolvers import reverse ...@@ -147,16 +147,19 @@ from django.core.urlresolvers import reverse
<section id="dashboard-search-results" class="search-results dashboard-search-results"></section> <section id="dashboard-search-results" class="search-results dashboard-search-results"></section>
% endif % 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"> <header class="profile">
<h2 class="username-header"><span class="sr">${_("Username")}: </span></h2> <h2 class="account-status-title sr">${_("Account Status Info")}: </h2>
</header> </header>
<section class="user-info"> <section class="user-info">
<ul> <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): % if len(order_history_list):
<li class="order-history"> <li class="order-history">
......
...@@ -53,6 +53,7 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -53,6 +53,7 @@ site_status_msg = get_site_status_msg(course_id)
% if user.is_authenticated(): % if user.is_authenticated():
% if not course or disable_courseware_header: % if not course or disable_courseware_header:
% if not nav_hidden:
<nav aria-label="Main" class="nav-main"> <nav aria-label="Main" class="nav-main">
<ul class="left nav-global authenticated"> <ul class="left nav-global authenticated">
<%block name="navigation_global_links_authenticated"> <%block name="navigation_global_links_authenticated">
...@@ -68,6 +69,7 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -68,6 +69,7 @@ site_status_msg = get_site_status_msg(course_id)
</%block> </%block>
</ul> </ul>
</nav> </nav>
% endif
% endif % endif
<ul class="user"> <ul class="user">
...@@ -101,44 +103,28 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -101,44 +103,28 @@ site_status_msg = get_site_status_msg(course_id)
% endif % endif
% else: % 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"> <nav aria-label="Account" class="nav-account-management">
<div class="right nav-courseware"> <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 not settings.FEATURES['DISABLE_LOGIN_BUTTON']:
% if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain: % if course and settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
<div class="nav-courseware-01"> <div class="nav-courseware-02">
<a class="cta cta-register" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a> <a class="cta cta-register nav-courseware-button" href="${reverse('course-specific-register', args=[course.id.to_deprecated_string()])}">${_("Register")}</a>
</div> </div>
% else: % else:
<div class="nav-courseware-01"> <div class="nav-courseware-02">
<a class="cta cta-register" href="/register">${_("Register")}</a> <a class="cta cta-register nav-courseware-button" href="/register">${_("Register")}</a>
</div> </div>
% endif % endif
% 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> </div>
</nav> </nav>
% endif % endif
......
...@@ -18,7 +18,6 @@ from django.utils.translation import ugettext as _ ...@@ -18,7 +18,6 @@ from django.utils.translation import ugettext as _
% endfor % endfor
</%block> </%block>
<%block name="js_extra"> <%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-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script>
......
...@@ -35,7 +35,6 @@ from verify_student.views import PayAndVerifyView ...@@ -35,7 +35,6 @@ from verify_student.views import PayAndVerifyView
% endfor % endfor
</%block> </%block>
<%block name="js_extra"> <%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-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script>
......
...@@ -16,7 +16,6 @@ from django.utils.translation import ugettext as _ ...@@ -16,7 +16,6 @@ from django.utils.translation import ugettext as _
% endfor % endfor
</%block> </%block>
<%block name="js_extra"> <%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-min.js')}"></script>
<script src="${static.url('js/vendor/underscore.string.min.js')}"></script> <script src="${static.url('js/vendor/underscore.string.min.js')}"></script>
<script src="${static.url('js/vendor/backbone-min.js')}"></script> <script src="${static.url('js/vendor/backbone-min.js')}"></script>
......
# -*- 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
...@@ -52,7 +52,7 @@ class CourseOverview(django.db.models.Model): ...@@ -52,7 +52,7 @@ class CourseOverview(django.db.models.Model):
cert_name_long = TextField() cert_name_long = TextField()
# Grading # 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 # Access parameters
days_early_for_beta = FloatField(null=True) days_early_for_beta = FloatField(null=True)
...@@ -77,6 +77,16 @@ class CourseOverview(django.db.models.Model): ...@@ -77,6 +77,16 @@ class CourseOverview(django.db.models.Model):
from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.certificates.api import get_active_web_certificate
from lms.djangoapps.courseware.courses import course_image_url 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( return CourseOverview(
id=course.id, id=course.id,
_location=course.location, _location=course.location,
...@@ -98,7 +108,7 @@ class CourseOverview(django.db.models.Model): ...@@ -98,7 +108,7 @@ class CourseOverview(django.db.models.Model):
has_any_active_web_certificate=(get_active_web_certificate(course) is not None), has_any_active_web_certificate=(get_active_web_certificate(course) is not None),
cert_name_short=course.cert_name_short, cert_name_short=course.cert_name_short,
cert_name_long=course.cert_name_long, 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, end_of_course_survey_url=course.end_of_course_survey_url,
days_early_for_beta=course.days_early_for_beta, days_early_for_beta=course.days_early_for_beta,
......
...@@ -294,3 +294,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase): ...@@ -294,3 +294,18 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
# which causes get_from_id to raise an IOError. # which causes get_from_id to raise an IOError.
with self.assertRaises(IOError): with self.assertRaises(IOError):
CourseOverview.get_from_id(course.id) 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)
...@@ -7,6 +7,7 @@ http://stackoverflow.com/questions/10060069/safely-extract-zip-or-tar-using-pyth ...@@ -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 os.path import abspath, realpath, dirname, join as joinpath
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.conf import settings
import logging import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -28,19 +29,23 @@ def _is_bad_path(path, base): ...@@ -28,19 +29,23 @@ def _is_bad_path(path, base):
def _is_bad_link(info, 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 # Links are interpreted relative to the directory containing the link
tip = resolved(joinpath(base, dirname(info.name))) tip = resolved(joinpath(base, dirname(info.name)))
return _is_bad_path(info.linkname, base=tip) 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. 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: for finfo in members:
if _is_bad_path(finfo.name, base): if _is_bad_path(finfo.name, base):
...@@ -61,8 +66,8 @@ def safemembers(members): ...@@ -61,8 +66,8 @@ def safemembers(members):
return 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))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment