Merge pull request #8969 from edx/release-merge-test
Release merge test
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"] = [
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
from nose.plugins.attrib import attr
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
import mock
|
||||
@@ -84,3 +85,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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
% 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>
|
||||
% else:
|
||||
<div class="nav-courseware-01">
|
||||
<a class="cta cta-register" href="/register">${_("Register")}</a>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
<div class="nav-courseware-02">
|
||||
<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 nav-courseware-button" href="${reverse('course-specific-login', args=[course.id.to_deprecated_string()])}${login_query()}">${_("Sign in")}</a>
|
||||
<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 nav-courseware-button" href="/login${login_query()}">${_("Sign in")}</a>
|
||||
<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-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-02">
|
||||
<a class="cta cta-register nav-courseware-button" href="/register">${_("Register")}</a>
|
||||
</div>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
</nav>
|
||||
% endif
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user