diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
index 60aed13c8d..faec60f3e8 100644
--- a/cms/djangoapps/contentstore/tests/test_contentstore.py
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -11,6 +11,7 @@ import json
from fs.osfs import OSFS
import copy
from json import loads
+import traceback
from django.contrib.auth.models import User
from django.dispatch import Signal
@@ -216,13 +217,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
module_store = modulestore('direct')
found = False
- item = None
items = module_store.get_items(['i4x', 'edX', 'full', 'poll_question', None, None])
found = len(items) > 0
self.assertTrue(found)
# check that there's actually content in the 'question' field
- self.assertGreater(len(items[0].question),0)
+ self.assertGreater(len(items[0].question), 0)
def test_xlint_fails(self):
err_cnt = perform_xlint('common/test/data', ['full'])
@@ -235,14 +235,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
sequential = module_store.get_item(Location(['i4x', 'edX', 'full', 'sequential', 'Administrivia_and_Circuit_Elements', None]))
- chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
+ chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertTrue(sequential.location.url() in chapter.children)
self.client.post(reverse('delete_item'),
json.dumps({'id': sequential.location.url(), 'delete_children': 'true', 'delete_all_versions': 'true'}),
- "application/json")
+ "application/json")
found = False
try:
@@ -253,7 +253,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.assertFalse(found)
- chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter','Week_1', None]))
+ chapter = module_store.get_item(Location(['i4x', 'edX', 'full', 'chapter', 'Week_1', None]))
# make sure the parent no longer points to the child object which was deleted
self.assertFalse(sequential.location.url() in chapter.children)
@@ -276,7 +276,6 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
import_from_xml(modulestore(), 'common/test/data/', ['full'])
module_store = modulestore('direct')
- content_store = contentstore()
source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
course = module_store.get_item(source_location)
@@ -289,7 +288,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
- }
+ }
import_from_xml(modulestore(), 'common/test/data/', ['full'])
@@ -348,17 +347,44 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
def test_export_course(self):
module_store = modulestore('direct')
+ draft_store = modulestore('draft')
content_store = contentstore()
import_from_xml(module_store, 'common/test/data/', ['full'])
location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+ # get a vertical (and components in it) to put into 'draft'
+ vertical = module_store.get_item(Location(['i4x', 'edX', 'full',
+ 'vertical', 'vertical_66', None]), depth=1)
+
+ draft_store.clone_item(vertical.location, vertical.location)
+
+ for child in vertical.get_children():
+ draft_store.clone_item(child.location, child.location)
+
root_dir = path(mkdtemp_clean())
+ # now create a private vertical
+ private_vertical = draft_store.clone_item(vertical.location,
+ Location(['i4x', 'edX', 'full', 'vertical', 'a_private_vertical', None]))
+
+ # add private to list of children
+ sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
+ 'sequential', 'Administrivia_and_Circuit_Elements', None]))
+ private_location_no_draft = private_vertical.location._replace(revision=None)
+ module_store.update_children(sequential.location, sequential.children +
+ [private_location_no_draft.url()])
+
+ # read back the sequential, to make sure we have a pointer to
+ sequential = module_store.get_item(Location(['i4x', 'edX', 'full',
+ 'sequential', 'Administrivia_and_Circuit_Elements', None]))
+
+ self.assertIn(private_location_no_draft.url(), sequential.children)
+
print 'Exporting to tempdir = {0}'.format(root_dir)
# export out to a tempdir
- export_to_xml(module_store, content_store, location, root_dir, 'test_export')
+ export_to_xml(module_store, content_store, location, root_dir, 'test_export', draft_modulestore=draft_store)
# check for static tabs
self.verify_content_existence(module_store, root_dir, location, 'tabs', 'static_tab', '.html')
@@ -392,20 +418,36 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
delete_course(module_store, content_store, location)
# reimport
- import_from_xml(module_store, root_dir, ['test_export'])
+ import_from_xml(module_store, root_dir, ['test_export'], draft_store=draft_store)
items = module_store.get_items(Location(['i4x', 'edX', 'full', 'vertical', None]))
self.assertGreater(len(items), 0)
for descriptor in items:
- print "Checking {0}....".format(descriptor.location.url())
- resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
- self.assertEqual(resp.status_code, 200)
+ # don't try to look at private verticals. Right now we're running
+ # the service in non-draft aware
+ if getattr(descriptor, 'is_draft', False):
+ print "Checking {0}....".format(descriptor.location.url())
+ resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
+ self.assertEqual(resp.status_code, 200)
+
+ # verify that we have the content in the draft store as well
+ vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
+ 'vertical', 'vertical_66', None]), depth=1)
+
+ self.assertTrue(getattr(vertical, 'is_draft', False))
+ for child in vertical.get_children():
+ self.assertTrue(getattr(child, 'is_draft', False))
+
+ # verify that we have the private vertical
+ test_private_vertical = draft_store.get_item(Location(['i4x', 'edX', 'full',
+ 'vertical', 'vertical_66', None]))
+
+ self.assertTrue(getattr(test_private_vertical, 'is_draft', False))
shutil.rmtree(root_dir)
def test_course_handouts_rewrites(self):
module_store = modulestore('direct')
- content_store = contentstore()
# import a test course
import_from_xml(module_store, 'common/test/data/', ['full'])
@@ -438,11 +480,11 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# make sure we pre-fetched a known sequential which should be at depth=2
self.assertTrue(Location(['i4x', 'edX', 'full', 'sequential',
- 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
+ 'Administrivia_and_Circuit_Elements', None]) in course.system.module_data)
# make sure we don't have a specific vertical which should be at depth=3
- self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58',
- None]) in course.system.module_data)
+ self.assertFalse(Location(['i4x', 'edX', 'full', 'vertical', 'vertical_58', None])
+ in course.system.module_data)
def test_export_course_with_unknown_metadata(self):
module_store = modulestore('direct')
@@ -469,10 +511,12 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
export_to_xml(module_store, content_store, location, root_dir, 'test_export')
exported = True
except Exception:
+ print 'Exception thrown: {0}'.format(traceback.format_exc())
pass
self.assertTrue(exported)
+
class ContentStoreTest(ModuleStoreTestCase):
"""
Tests for the CMS ContentStore application.
@@ -507,7 +551,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'org': 'MITx',
'number': '999',
'display_name': 'Robot Super Course',
- }
+ }
def test_create_course(self):
"""Test new course creation - happy path"""
@@ -534,7 +578,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
- 'There is already a course defined with the same organization and course number.')
+ 'There is already a course defined with the same organization and course number.')
def test_create_course_with_bad_organization(self):
"""Test new course creation - error path for bad organization name"""
@@ -544,7 +588,7 @@ class ContentStoreTest(ModuleStoreTestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(data['ErrMsg'],
- "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
+ "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
def test_course_index_view_with_no_courses(self):
"""Test viewing the index page with no courses"""
@@ -580,10 +624,10 @@ class ContentStoreTest(ModuleStoreTestCase):
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
data = {
- 'org': 'MITx',
- 'course': '999',
- 'name': Location.clean('Robot Super Course'),
- }
+ 'org': 'MITx',
+ 'course': '999',
+ 'name': Location.clean('Robot Super Course'),
+ }
resp = self.client.get(reverse('course_index', kwargs=data))
self.assertContains(resp,
@@ -599,7 +643,7 @@ class ContentStoreTest(ModuleStoreTestCase):
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/chapter/Empty',
'display_name': 'Section One',
- }
+ }
resp = self.client.post(reverse('clone_item'), section_data)
@@ -615,7 +659,7 @@ class ContentStoreTest(ModuleStoreTestCase):
problem_data = {
'parent_location': 'i4x://MITx/999/course/Robot_Super_Course',
'template': 'i4x://edx/templates/problem/Blank_Common_Problem'
- }
+ }
resp = self.client.post(reverse('clone_item'), problem_data)
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 227379979e..3169b437ed 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -1586,7 +1586,8 @@ def import_course(request, org, course, name):
shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
- [course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location))
+ [course_subdir], load_error_modules=False, static_content_store=contentstore(),
+ target_location_namespace=Location(location), draft_store=modulestore())
# we can blow this away when we're done importing.
shutil.rmtree(course_dir)
@@ -1620,8 +1621,8 @@ def generate_export_course(request, org, course, name):
logging.debug('root = {0}'.format(root_dir))
- export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name)
- # filename = root_dir / name + '.tar.gz'
+ export_to_xml(modulestore('direct'), contentstore(), loc, root_dir, name, modulestore())
+ #filename = root_dir / name + '.tar.gz'
logging.debug('tar file being generated at {0}'.format(export_file.name))
tf = tarfile.open(name=export_file.name, mode='w:gz')
diff --git a/cms/static/js/base.js b/cms/static/js/base.js
index 4140beb2da..51d358d0eb 100644
--- a/cms/static/js/base.js
+++ b/cms/static/js/base.js
@@ -225,7 +225,6 @@ function toggleSections(e) {
function editSectionPublishDate(e) {
e.preventDefault();
$modal = $('.edit-subsection-publish-settings').show();
- $modal = $('.edit-subsection-publish-settings').show();
$modal.attr('data-id', $(this).attr('data-id'));
$modal.find('.start-date').val($(this).attr('data-date'));
$modal.find('.start-time').val($(this).attr('data-time'));
diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss
index c4e96616a8..953b2d15e5 100644
--- a/cms/static/sass/elements/_controls.scss
+++ b/cms/static/sass/elements/_controls.scss
@@ -97,7 +97,7 @@
color: $blue;
&:hover, &:active {
- background: $blue-l3;
+ background: $blue-l4;
color: $blue-s2;
}
diff --git a/cms/static/sass/elements/_forms.scss b/cms/static/sass/elements/_forms.scss
index 384ffc0509..1faf4a883e 100644
--- a/cms/static/sass/elements/_forms.scss
+++ b/cms/static/sass/elements/_forms.scss
@@ -8,11 +8,11 @@ input[type="password"],
textarea.text {
padding: 6px 8px 8px;
@include box-sizing(border-box);
- border: 1px solid $mediumGrey;
+ border: 1px solid $gray-l2;
border-radius: 2px;
- @include linear-gradient($lightGrey, tint($lightGrey, 90%));
- background-color: $lightGrey;
- @include box-shadow(0 1px 2px rgba(0, 0, 0, .1) inset);
+ @include linear-gradient($gray-l5, $white);
+ background-color: $gray-l5;
+ @include box-shadow(inset 0 1px 2px $shadow-l1);
font-family: 'Open Sans', sans-serif;
font-size: 11px;
color: $baseFontColor;
@@ -21,7 +21,7 @@ textarea.text {
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
- color: #979faf;
+ color: $gray-l2;
}
&:focus {
@@ -30,7 +30,72 @@ textarea.text {
}
}
-// forms - specific
+// ====================
+
+// forms - fields - not editable
+.field.is-not-editable {
+
+ & label.is-focused {
+ color: $gray-d2;
+ }
+
+ label, input, textarea {
+ pointer-events: none;
+ }
+}
+
+// ====================
+
+// field with error
+.field.error {
+
+ input, textarea {
+ border-color: $red;
+ }
+}
+
+// ====================
+
+// forms - additional UI
+form {
+
+ .note {
+ @include box-sizing(border-box);
+
+ .title {
+
+ }
+
+ .copy {
+
+ }
+
+ // note with actions
+ &.has-actions {
+ @include clearfix();
+
+ .title {
+
+ }
+
+ .copy {
+
+ }
+
+ .list-actions {
+
+ }
+ }
+ }
+
+ .note-promotion {
+
+ }
+}
+
+// ====================
+
+// forms - grandfathered
input.search {
padding: 6px 15px 8px 30px;
@include box-sizing(border-box);
@@ -73,4 +138,4 @@ code {
background-color: #edf1f5;
@include box-shadow(0 1px 2px rgba(0, 0, 0, 0.1) inset);
font-family: Monaco, monospace;
-}
\ No newline at end of file
+}
diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss
index 307ebad0a8..d1ba706d56 100644
--- a/cms/static/sass/views/_settings.scss
+++ b/cms/static/sass/views/_settings.scss
@@ -147,7 +147,7 @@ body.course.settings {
}
label {
- @include font-size(14);
+ @extend .t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
margin: 0 0 ($baseline/4) 0;
font-weight: 400;
@@ -161,7 +161,7 @@ body.course.settings {
@include placeholder($gray-l4);
@include font-size(16);
@include size(100%,100%);
- padding: ($baseline/2);
+ padding: ($baseline/2);
&.long {
}
@@ -212,7 +212,7 @@ body.course.settings {
padding: $baseline;
&:last-child {
- padding-bottom: $baseline;
+ padding-bottom: $baseline;
}
.actions {
@@ -238,33 +238,36 @@ body.course.settings {
}
}
- // not editable fields
- .field.is-not-editable {
-
- & label.is-focused {
- color: $gray-d2;
- }
- }
-
- // field with error
- .field.error {
-
- input, textarea {
- border-color: $red;
- }
- }
-
// specific fields - basic
&.basic {
.list-input {
@include clearfix();
+ padding: 0 ($baseline/2);
.field {
margin-bottom: 0;
}
}
+ // course details that should appear more like content than elements to change
+ .field.is-not-editable {
+
+ label {
+
+ }
+
+ input, textarea {
+ @extend .t-copy-lead1;
+ @include box-shadow(none);
+ border: none;
+ background: none;
+ padding: 0;
+ margin: 0;
+ font-weight: 600;
+ }
+ }
+
#field-course-organization {
float: left;
width: flex-grid(2, 9);
@@ -281,6 +284,58 @@ body.course.settings {
float: left;
width: flex-grid(5, 9);
}
+
+ // course link note
+ .note-promotion-courseURL {
+ @include box-shadow(0 2px 1px $shadow-l1);
+ @include border-radius(($baseline/5));
+ margin-top: ($baseline*1.5);
+ border: 1px solid $gray-l2;
+ padding: ($baseline/2) 0 0 0;
+
+ .title {
+ @extend .t-copy-sub1;
+ margin: 0 0 ($baseline/10) 0;
+ padding: 0 ($baseline/2);
+
+ .tip {
+ display: inline;
+ margin-left: ($baseline/4);
+ }
+ }
+
+ .copy {
+ padding: 0 ($baseline/2) ($baseline/2) ($baseline/2);
+
+ .link-courseURL {
+ @extend .t-copy-lead1;
+
+ &:hover {
+
+ }
+ }
+ }
+
+ .list-actions {
+ @include box-shadow(inset 0 1px 1px $shadow-l1);
+ border-top: 1px solid $gray-l2;
+ padding: ($baseline/2);
+ background: $gray-l5;
+
+ .action-primary {
+ @include blue-button();
+ @include font-size(13);
+ font-weight: 600;
+
+ .icon {
+ @extend .t-icon;
+ @include font-size(16);
+ display: inline-block;
+ vertical-align: middle;
+ }
+ }
+ }
+ }
}
// specific fields - schedule
@@ -322,7 +377,7 @@ body.course.settings {
}
}
}
-
+
// specific fields - overview
#field-course-overview {
@@ -468,7 +523,7 @@ body.course.settings {
}
}
}
-
+
.grade-specific-bar {
height: 50px !important;
}
@@ -479,7 +534,7 @@ body.course.settings {
li {
position: absolute;
top: 0;
- height: 50px;
+ height: 50px;
text-align: right;
@include border-radius(2px);
@@ -600,8 +655,8 @@ body.course.settings {
}
#field-course-grading-assignment-shortname,
- #field-course-grading-assignment-totalassignments,
- #field-course-grading-assignment-gradeweight,
+ #field-course-grading-assignment-totalassignments,
+ #field-course-grading-assignment-gradeweight,
#field-course-grading-assignment-droppable {
width: flex-grid(2, 6);
}
@@ -734,4 +789,4 @@ body.course.settings {
.content-supplementary {
width: flex-grid(3, 12);
}
-}
\ No newline at end of file
+}
diff --git a/cms/templates/settings.html b/cms/templates/settings.html
index e4cb4b3743..cc5dafc57b 100644
--- a/cms/templates/settings.html
+++ b/cms/templates/settings.html
@@ -4,7 +4,7 @@
<%namespace name='static' file='static_content.html'/>
<%!
-from contentstore import utils
+from contentstore import utils
%>
@@ -13,17 +13,17 @@ from contentstore import utils
-
+
-
+
%block>
@@ -62,10 +62,10 @@ from contentstore import utils
Your course's schedule settings determine when students can enroll in and begin a course as well as when the course.
Additionally, details provided on this page are also used in edX's catalog of courses, which new and returning students use to choose new courses to study.
- +