diff --git a/cms/djangoapps/contentstore/features/course-outline.py b/cms/djangoapps/contentstore/features/course-outline.py
index f3203880aacb..37bf8eb16e1e 100644
--- a/cms/djangoapps/contentstore/features/course-outline.py
+++ b/cms/djangoapps/contentstore/features/course-outline.py
@@ -131,14 +131,3 @@ def all_sections_are_collapsed_or_expanded(step, text):
def change_grading_status(step):
world.css_find('a.menu-toggle').click()
world.css_find('.menu li').first.click()
-
-
-@step(u'I reorder subsections')
-def reorder_subsections(_step):
- draggable_css = '.subsection-drag-handle'
- ele = world.css_find(draggable_css).first
- ele.action_chains.drag_and_drop_by_offset(
- ele._element,
- 0,
- 25
- ).perform()
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index 462e30076178..a7d20a858699 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -282,7 +282,7 @@ def find_xblock_info(xblock_info, locator):
"""
if xblock_info['id'] == locator:
return xblock_info
- children = xblock_info['child_info']['children'] if xblock_info['child_info'] else None
+ children = xblock_info['child_info']['children'] if xblock_info.get('child_info', None) else None
if children:
for child_xblock_info in children:
result = find_xblock_info(child_xblock_info, locator)
@@ -295,7 +295,7 @@ def collect_all_locators(locators, xblock_info):
Collect all the locators for an xblock and its children.
"""
locators.append(xblock_info['id'])
- children = xblock_info['child_info']['children'] if xblock_info['child_info'] else None
+ children = xblock_info['child_info']['children'] if xblock_info.get('child_info', None) else None
if children:
for child_xblock_info in children:
collect_all_locators(locators, child_xblock_info)
diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py
index 7a7144ff280a..64307069ea00 100644
--- a/cms/djangoapps/contentstore/views/item.py
+++ b/cms/djangoapps/contentstore/views/item.py
@@ -606,10 +606,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
In addition, an optional include_children_predicate argument can be provided to define whether or
not a particular xblock should have its children included.
"""
- published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
# Treat DEFAULT_START_DATE as a magic number that means the release date has not been set
- release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
def safe_get_username(user_id):
"""
@@ -628,12 +626,27 @@ def safe_get_username(user_id):
return None
+ # Compute the child info first so it can be included in aggregate information for the parent
+ if include_child_info and xblock.has_children:
+ child_info = _create_xblock_child_info(
+ xblock, include_children_predicate=include_children_predicate
+ )
+ else:
+ child_info = None
+
+ visible_to_staff_only = _is_visible_to_staff_only(xblock, child_info)
+ release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
+ currently_visible_to_students = is_currently_visible_to_students(xblock)
+ fully_visible_to_students = currently_visible_to_students
+ if fully_visible_to_students and not visible_to_staff_only and child_info:
+ fully_visible_to_students = all(
+ (info['fully_visible_to_students'] or info['visible_to_staff_only']) for info in child_info['children']
+ )
+
xblock_info = {
"id": unicode(xblock.location),
"display_name": xblock.display_name_with_default,
"category": xblock.category,
- "has_changes": modulestore().has_changes(xblock.location),
- "published": published,
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
"edited_by": safe_get_username(xblock.subtree_edited_by),
"published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None,
@@ -642,8 +655,11 @@ def safe_get_username(user_id):
"released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date,
"release_date_from": _get_release_date_from(xblock) if release_date else None,
- "visible_to_staff_only": xblock.visible_to_staff_only,
- "currently_visible_to_students": is_currently_visible_to_students(xblock),
+ "visible_to_staff_only": visible_to_staff_only,
+ "currently_visible_to_students": currently_visible_to_students,
+ "fully_published": _is_fully_published(xblock, child_info),
+ "fully_visible_to_students": fully_visible_to_students,
+ "has_unpublished_content": not visible_to_staff_only and _has_unpublished_content(xblock, child_info),
}
if data is not None:
xblock_info["data"] = data
@@ -651,13 +667,53 @@ def safe_get_username(user_id):
xblock_info["metadata"] = metadata
if include_ancestor_info:
xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock)
- if include_child_info and xblock.has_children:
- xblock_info['child_info'] = _create_xblock_child_info(
- xblock, include_children_predicate=include_children_predicate
- )
+ if child_info:
+ xblock_info['child_info'] = child_info
return xblock_info
+def _is_visible_to_staff_only(xblock, child_info):
+ """
+ Returns true if the specified xblock and all of its children are shown to staff only.
+ """
+ if xblock.visible_to_staff_only:
+ return True
+ elif child_info and len(child_info['children']) > 0:
+ return all(info['visible_to_staff_only'] for info in child_info['children'])
+ return False
+
+
+def _has_unpublished_content(xblock, child_info):
+ """
+ Returns true if the xblock or its children have unpublished content (that is not staff only)
+ """
+ if is_unit(xblock):
+ return modulestore().has_changes(xblock.location)
+ elif child_info:
+ return any(info['has_unpublished_content'] for info in child_info['children'])
+ else:
+ return False
+
+
+def _is_fully_published(xblock, child_info):
+ """
+ Returns true if the specified xblock and all of its children are published (or marked as staff only).
+ """
+ if is_unit(xblock):
+ return (
+ modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
+ if is_unit(xblock)
+ else False
+ )
+ elif child_info and len(child_info['children']) > 0:
+ return all(
+ (info['fully_published'] or info['visible_to_staff_only']) for info in child_info['children']
+ )
+ else:
+ return False
+
+
+
def _create_xblock_ancestor_info(xblock):
"""
Returns information about the ancestors of an xblock. Note that the direct parent will also return
diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py
index 85d251f468a4..8639ce1078ae 100644
--- a/cms/djangoapps/contentstore/views/tests/test_item.py
+++ b/cms/djangoapps/contentstore/views/tests/test_item.py
@@ -1,7 +1,7 @@
"""Tests for items views."""
import json
-from datetime import datetime
+from datetime import datetime, timedelta
import ddt
from mock import patch
@@ -1248,3 +1248,82 @@ def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False,
)
else:
self.assertIsNone(xblock_info.get('child_info', None))
+
+
+class TestXBlockPublishingInfo(ItemTest):
+ """
+ Unit tests for XBlock's outline handling.
+ """
+ def _create_child(self, parent, category, display_name, publish_item=False):
+ return ItemFactory.create(
+ parent_location=parent.location, category=category, display_name=display_name,
+ user_id=self.user.id, publish_item=publish_item
+ )
+
+ def get_child(self, xblock_info, index):
+ """
+ Returns the child at the specified index.
+ """
+ children = xblock_info['child_info']['children']
+ self.assertTrue(len(children) > index)
+ return children[index]
+
+ def test_empty_chapter_publishing_info(self):
+ empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter")
+ xblock_info = create_xblock_info(
+ modulestore().get_item(empty_chapter.location),
+ include_child_info=True,
+ include_children_predicate=ALWAYS,
+ )
+ self.validate_publishing_info(xblock_info)
+
+ def test_empty_section_publishing_info(self):
+ chapter = self._create_child(self.course, 'chapter', "Test Chapter")
+ self._create_child(chapter, 'sequential', "Empty Sequential")
+ xblock_info = create_xblock_info(
+ modulestore().get_item(chapter.location),
+ include_child_info=True,
+ include_children_predicate=ALWAYS,
+ )
+ self.validate_publishing_info(xblock_info)
+ self.validate_publishing_info(self.get_child(xblock_info, 0))
+
+ def test_published_unit_publishing_info(self):
+ chapter = self._create_child(self.course, 'chapter', "Test Chapter")
+ sequential = self._create_child(chapter, 'sequential', "Test Sequential")
+ self._create_child(sequential, 'vertical', "Published Unit", publish_item=True)
+ xblock_info = create_xblock_info(
+ modulestore().get_item(chapter.location),
+ include_child_info=True,
+ include_children_predicate=ALWAYS,
+ )
+ self.validate_publishing_info(xblock_info, fully_published=True)
+ sequential_child_info = self.get_child(xblock_info, 0)
+ self.validate_publishing_info(sequential_child_info, fully_published=True)
+ unit_child_info = self.get_child(sequential_child_info, 0)
+ self.validate_publishing_info(unit_child_info, fully_published=True)
+
+ def test_released_unit_publishing_info(self):
+ chapter = self._create_child(self.course, 'chapter', "Test Chapter")
+ sequential = self._create_child(chapter, 'sequential', "Test Sequential")
+ unit = self._create_child(sequential, 'vertical', "Published Unit", publish_item=True)
+ chapter_block = modulestore().get_item(chapter.location)
+ chapter_block.start = datetime.now(UTC) - timedelta(days=1)
+ self.store.publish(unit.location, self.user.id)
+ xblock_info = create_xblock_info(
+ chapter_block,
+ include_child_info=True,
+ include_children_predicate=ALWAYS,
+ )
+ self.validate_publishing_info(xblock_info, fully_visible_to_students=True, fully_published=True)
+ sequential_child_info = self.get_child(xblock_info, 0)
+ self.validate_publishing_info(sequential_child_info, fully_visible_to_students=True, fully_published=True)
+ unit_child_info = self.get_child(sequential_child_info, 0)
+ self.validate_publishing_info(unit_child_info, fully_visible_to_students=True, fully_published=True)
+
+ def validate_publishing_info(self, xblock_info, fully_visible_to_students=False, fully_published=False,
+ is_staff_only=False, release_date=None):
+ self.assertEqual(xblock_info.get('fully_published', False), fully_published)
+ self.assertEqual(xblock_info.get('fully_visible_to_students', False), fully_visible_to_students)
+ self.assertEqual(xblock_info.get('visible_to_staff_only', False), is_staff_only)
+ self.assertEqual(xblock_info.get('release_date', False), release_date)
diff --git a/cms/static/js/models/xblock_info.js b/cms/static/js/models/xblock_info.js
index 8cee477c0e14..86edeafa64d4 100644
--- a/cms/static/js/models/xblock_info.js
+++ b/cms/static/js/models/xblock_info.js
@@ -31,9 +31,19 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"has_changes": null,
/**
- * True iff a published version of the xblock exists.
+ * True if either:
+ * 1) The block itself has never been published.
+ * 2) At least one of the block's children has never been published.
*/
- "published": null,
+ "has_unpublished_content": null,
+ /**
+ * True iff the xblock and all of its children are published (excluding staff-only items)
+ */
+ "fully_published": null,
+ /**
+ * True iff the xblock and all of its children are visible to students (excluding staff-only items)
+ */
+ "fully_visible_to_students": null,
/**
* If true, only course staff can see the xblock regardless of publish status or
* release date status.
diff --git a/cms/static/js/spec/views/pages/course_outline_spec.js b/cms/static/js/spec/views/pages/course_outline_spec.js
index 727b183dc16d..fcb5997ab1d6 100644
--- a/cms/static/js/spec/views/pages/course_outline_spec.js
+++ b/cms/static/js/spec/views/pages/course_outline_spec.js
@@ -4,7 +4,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
describe("CourseOutlinePage", function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests,
- getHeaderElement, expandAndVerifyState, collapseAndVerifyState,
+ getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
@@ -64,21 +64,28 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
};
};
- getHeaderElement = function(selector) {
- var element = outlinePage.$(selector);
- return element.find('> .wrapper-xblock-header');
+ getItemsOfType = function(type) {
+ return outlinePage.$('.outline-' + type);
};
- expandAndVerifyState = function(selector) {
- var element = outlinePage.$(selector);
- getHeaderElement(selector).find('.ui-toggle-expansion').click();
- expect(element).not.toHaveClass('collapsed');
+ getItemHeaders = function(type) {
+ return getItemsOfType(type).find('> .' + type + '-header');
};
- collapseAndVerifyState = function(selector) {
- var element = outlinePage.$(selector);
- getHeaderElement(selector).find('.ui-toggle-expansion').click();
- expect(element).toHaveClass('collapsed');
+ verifyItemsExpanded = function(type, isExpanded) {
+ var element = getItemsOfType(type);
+ expect(element).toHaveClass(isExpanded ? 'is-expanded' : 'is-collapsed');
+ expect(element).not.toHaveClass(isExpanded ? 'is-collapsed' : 'is-expanded');
+ };
+
+ expandItemsAndVerifyState = function(type) {
+ getItemHeaders(type).find('.ui-toggle-expansion').click();
+ verifyItemsExpanded(type, true);
+ };
+
+ collapseItemsAndVerifyState = function(type) {
+ getItemHeaders(type).find('.ui-toggle-expansion').click();
+ verifyItemsExpanded(type, false);
};
createCourseOutlinePage = function(test, courseJSON, createOnly) {
@@ -129,9 +136,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
describe('Initial display', function() {
it('can render itself', function() {
createCourseOutlinePage(this, mockCourseJSON);
- expect(outlinePage.$('.sortable-course-list')).toExist();
- expect(outlinePage.$('.sortable-section-list')).toExist();
- expect(outlinePage.$('.sortable-subsection-list')).toExist();
+ expect(outlinePage.$('.list-sections')).toExist();
+ expect(outlinePage.$('.list-subsections')).toExist();
+ expect(outlinePage.$('.list-units')).toExist();
});
it('shows a loading indicator', function() {
@@ -142,18 +149,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
});
it('shows subsections initially collapsed', function() {
- var subsectionElement;
createCourseOutlinePage(this, mockCourseJSON);
- subsectionElement = outlinePage.$('.outline-item-subsection');
- expect(subsectionElement).toHaveClass('collapsed');
- expect(outlinePage.$('.outline-item-unit')).not.toExist();
+ verifyItemsExpanded('subsection', false);
+ expect(getItemsOfType('unit')).not.toExist();
});
});
describe("Button bar", function() {
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
- outlinePage.$('.nav-actions .add-button').click();
+ outlinePage.$('.nav-actions .button-new').click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'category': 'chapter',
'display_name': 'Section',
@@ -166,13 +171,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
- expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section');
+ expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section');
});
it('can add a second section', function() {
var sectionElements;
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
- outlinePage.$('.nav-actions .add-button').click();
+ outlinePage.$('.nav-actions .button-new').click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'category': 'chapter',
'display_name': 'Section',
@@ -186,7 +191,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section-2', 'Mock Section 2', []));
- sectionElements = outlinePage.$('.sortable-course-list .outline-item-section');
+ sectionElements = getItemsOfType('section');
expect(sectionElements.length).toBe(2);
expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
expect($(sectionElements[1]).data('locator')).toEqual('mock-section-2');
@@ -194,10 +199,11 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can expand and collapse all sections', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
- outlinePage.$('.nav-actions .toggle-button-expand-collapse').click();
- expect(outlinePage.$('.outline-item-section')).toHaveClass('collapsed');
- outlinePage.$('.nav-actions .toggle-button-expand-collapse').click();
- expect(outlinePage.$('.outline-item-section')).not.toHaveClass('collapsed');
+ verifyItemsExpanded('section', true);
+ outlinePage.$('.nav-actions .button-toggle-expand-collapse .collapse-all').click();
+ verifyItemsExpanded('section', false);
+ outlinePage.$('.nav-actions .button-toggle-expand-collapse .expand-all').click();
+ verifyItemsExpanded('section', true);
});
});
@@ -205,12 +211,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('shows an empty course message initially', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
- expect(outlinePage.$('.no-content .add-button')).toExist();
+ expect(outlinePage.$('.no-content .button-new')).toExist();
});
it('can add a section', function() {
createCourseOutlinePage(this, mockEmptyCourseJSON);
- $('.no-content .add-button').click();
+ $('.no-content .button-new').click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'category': 'chapter',
'display_name': 'Section',
@@ -223,13 +229,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
create_sinon.respondWithJson(requests, mockSingleSectionCourseJSON);
expect(outlinePage.$('.no-content')).not.toExist();
- expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section');
+ expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section');
});
it('remains empty if an add fails', function() {
var requestCount;
createCourseOutlinePage(this, mockEmptyCourseJSON);
- $('.no-content .add-button').click();
+ $('.no-content .button-new').click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'category': 'chapter',
'display_name': 'Section',
@@ -239,7 +245,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.respondWithError(requests);
expect(requests.length).toBe(requestCount); // No additional requests should be made
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
- expect(outlinePage.$('.no-content .add-button')).toExist();
+ expect(outlinePage.$('.no-content .button-new')).toExist();
});
});
@@ -247,7 +253,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
var getDisplayNameWrapper;
getDisplayNameWrapper = function() {
- return getHeaderElement('.outline-item-section').find('.wrapper-xblock-field').first();
+ return getItemHeaders('section').find('.wrapper-xblock-field');
};
it('can be deleted', function() {
@@ -256,7 +262,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createMockSectionJSON('mock-section', 'Mock Section', []),
createMockSectionJSON('mock-section-2', 'Mock Section 2', [])
]));
- outlinePage.$('.outline-item-section .delete-button').first().click();
+ getItemHeaders('section').find('.delete-button').click();
view_helpers.confirmPrompt(promptSpy);
requestCount = requests.length;
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
@@ -269,32 +275,32 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be deleted if it is the only section', function() {
var promptSpy = view_helpers.createPromptSpy();
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
- outlinePage.$('.outline-item-section .delete-button').click();
+ getItemHeaders('section').find('.delete-button').click();
view_helpers.confirmPrompt(promptSpy);
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
create_sinon.respondWithJson(requests, {});
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-course');
create_sinon.respondWithJson(requests, mockEmptyCourseJSON);
expect(outlinePage.$('.no-content')).not.toHaveClass('is-hidden');
- expect(outlinePage.$('.no-content .add-button')).toExist();
+ expect(outlinePage.$('.no-content .button-new')).toExist();
});
it('remains visible if its deletion fails', function() {
var promptSpy = view_helpers.createPromptSpy(),
requestCount;
createCourseOutlinePage(this, mockSingleSectionCourseJSON);
- outlinePage.$('.outline-item-section .delete-button').click();
+ getItemHeaders('section').find('.delete-button').click();
view_helpers.confirmPrompt(promptSpy);
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-section');
requestCount = requests.length;
create_sinon.respondWithError(requests);
expect(requests.length).toBe(requestCount); // No additional requests should be made
- expect(outlinePage.$('.sortable-course-list li').data('locator')).toEqual('mock-section');
+ expect(outlinePage.$('.list-sections li').data('locator')).toEqual('mock-section');
});
it('can add a subsection', function() {
createCourseOutlinePage(this, mockCourseJSON);
- outlinePage.$('.outline-item-section > .add-xblock-component .add-button').click();
+ getItemsOfType('section').find('> .outline-content > .add-subsection .button-new').click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'category': 'sequential',
'display_name': 'Subsection',
@@ -329,9 +335,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be expanded and collapsed', function() {
createCourseOutlinePage(this, mockCourseJSON);
- collapseAndVerifyState('.outline-item-section');
- expandAndVerifyState('.outline-item-section');
- collapseAndVerifyState('.outline-item-section');
+ collapseItemsAndVerifyState('section');
+ expandItemsAndVerifyState('section');
+ collapseItemsAndVerifyState('section');
});
});
@@ -339,13 +345,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
var getDisplayNameWrapper;
getDisplayNameWrapper = function() {
- return getHeaderElement('.outline-item-subsection').find('.wrapper-xblock-field').first();
+ return getItemHeaders('subsection').find('.wrapper-xblock-field');
};
it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON);
- getHeaderElement('.outline-item-subsection').find('.delete-button').click();
+ getItemHeaders('subsection').find('.delete-button').click();
view_helpers.confirmPrompt(promptSpy);
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-subsection');
create_sinon.respondWithJson(requests, {});
@@ -358,7 +364,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
var redirectSpy;
createCourseOutlinePage(this, mockCourseJSON);
redirectSpy = spyOn(ViewUtils, 'redirect');
- outlinePage.$('.outline-item-subsection > .add-xblock-component .add-button').click();
+ getItemsOfType('subsection').find('> .outline-content > .add-subsection .button-new').click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/', {
'category': 'vertical',
'display_name': 'Unit',
@@ -387,20 +393,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createMockSubsectionJSON('mock-subsection', updatedDisplayName, [])
]));
// Find the display name again in the refreshed DOM and verify it
- displayNameWrapper = getHeaderElement('.outline-item-subsection').find('.wrapper-xblock-field').first();
+ displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
subsectionModel = outlinePage.model.get('child_info').children[0].get('child_info').children[0];
expect(subsectionModel.get('display_name')).toBe(updatedDisplayName);
});
it('can be expanded and collapsed', function() {
- var subsectionElement;
createCourseOutlinePage(this, mockCourseJSON);
- subsectionElement = outlinePage.$('.outline-item-subsection');
- expect(subsectionElement).toHaveClass('collapsed');
- expandAndVerifyState('.outline-item-subsection');
- collapseAndVerifyState('.outline-item-subsection');
- expandAndVerifyState('.outline-item-subsection');
+ verifyItemsExpanded('subsection', false);
+ expandItemsAndVerifyState('subsection');
+ collapseItemsAndVerifyState('subsection');
+ expandItemsAndVerifyState('subsection');
});
});
@@ -409,8 +413,8 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy();
createCourseOutlinePage(this, mockCourseJSON);
- expandAndVerifyState('.outline-item-subsection');
- getHeaderElement('.outline-item-unit').find('.delete-button').click();
+ expandItemsAndVerifyState('subsection');
+ getItemHeaders('unit').find('.delete-button').click();
view_helpers.confirmPrompt(promptSpy);
create_sinon.expectJsonRequest(requests, 'DELETE', '/xblock/mock-unit');
create_sinon.respondWithJson(requests, {});
@@ -420,12 +424,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
});
it('has a link to the unit page', function() {
- var anchor;
+ var unitAnchor;
createCourseOutlinePage(this, mockCourseJSON);
- expandAndVerifyState('.outline-item-subsection');
- anchor = outlinePage.$('.outline-item-unit .xblock-title a');
- expect(anchor.attr('href')).toBe('/container/mock-unit');
+ expandItemsAndVerifyState('subsection');
+ unitAnchor = getItemsOfType('unit').find('.xblock-title a');
+ expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
});
});
+
+ describe("Publishing State", function() {
+ // TODO: implement this!!!!
+ });
});
});
diff --git a/cms/static/js/spec/views/unit_outline_spec.js b/cms/static/js/spec/views/unit_outline_spec.js
index 5396ce896c9d..44f8462b9822 100644
--- a/cms/static/js/spec/views/unit_outline_spec.js
+++ b/cms/static/js/spec/views/unit_outline_spec.js
@@ -77,9 +77,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can render itself', function() {
createUnitOutlineView(this, createMockXBlockInfo('Mock Unit'));
- expect(unitOutlineView.$('.sortable-course-list')).toExist();
- expect(unitOutlineView.$('.sortable-section-list')).toExist();
- expect(unitOutlineView.$('.sortable-subsection-list')).toExist();
+ expect(unitOutlineView.$('.list-sections')).toExist();
+ expect(unitOutlineView.$('.list-subsections')).toExist();
+ expect(unitOutlineView.$('.list-units')).toExist();
});
it('can add a unit', function() {
diff --git a/cms/static/js/utils/drag_and_drop.js b/cms/static/js/utils/drag_and_drop.js
index 2479ec47ba7a..3d9282ccb764 100644
--- a/cms/static/js/utils/drag_and_drop.js
+++ b/cms/static/js/utils/drag_and_drop.js
@@ -6,6 +6,8 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
droppableClasses: 'drop-target drop-target-prepend drop-target-before drop-target-after',
validDropClass: "valid-drop",
expandOnDropClass: "expand-on-drop",
+ collapsedClass: "is-collapsed",
+ expandedClass: "is-expanded",
/*
* Determine information about where to drop the currently dragged
@@ -28,7 +30,7 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
// element is on top of its parent list -- don't check the
// position of the container
var parentList = container.parents(ele.data('parent-location-selector')).first();
- if (parentList.hasClass('collapsed')) {
+ if (parentList.hasClass(this.collapsedClass)) {
var parentListTop = parentList.offset().top;
// To make it easier to drop subsections into collapsed sections (which have
// a lot of visual padding around them), allow a fudge factor around the
@@ -158,8 +160,9 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
// The direction the drag is moving in (negative means up, positive down).
dragDirection: 0
};
- if (!ele.hasClass('collapsed')) {
- ele.addClass('collapsed');
+ if (ele.hasClass(this.expandedClass)) {
+ ele.removeClass(this.expandedClass);
+ ele.addClass(this.collapsedClass);
ele.find('.expand-collapse').first().addClass('expand').removeClass('collapse');
// onDragStart gets called again after the collapse, so we can't just store a variable in the dragState.
ele.addClass(this.expandOnDropClass);
@@ -251,11 +254,15 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
},
pointerInBounds: function (pointer, ele) {
- return pointer.clientX >= ele.offset().left && pointer.clientX < ele.offset().left + ele.width();
+// console.log("pointer.clientX " + pointer.clientX);
+// console.log("ele.offset().left " + ele.offset().left);
+// console.log("ele.outerWidth() " + ele.outerWidth());
+ return pointer.clientX >= ele.offset().left && pointer.clientX < ele.offset().left + ele.outerWidth();
},
expandElement: function (ele) {
- ele.removeClass('collapsed');
+ ele.removeClass(this.collapsedClass);
+ ele.addClass(this.expandedClass);
ele.find('.expand-collapse').first().removeClass('expand').addClass('collapse');
},
@@ -322,21 +329,25 @@ define(["jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notif
* into `droppableClass`, and with parent type
* `parentLocationSelector`.
*/
- makeDraggable: function (type, handleClass, droppableClass, parentLocationSelector) {
+ makeDraggable: function (type, handleClass, droppableClass, parentLocationSelector, el) {
+ console.log("makeDraggable on " + type);
_.each(
- $(type),
+ el ? el : $(type),
function (ele) {
// Remember data necessary to reconstruct the parent-child relationships
- $(ele).data('droppable-class', droppableClass);
- $(ele).data('parent-location-selector', parentLocationSelector);
- $(ele).data('child-selector', type);
- var draggable = new Draggabilly(ele, {
- handle: handleClass,
- containment: '.wrapper-dnd'
- });
- draggable.on('dragStart', _.bind(contentDragger.onDragStart, contentDragger));
- draggable.on('dragMove', _.bind(contentDragger.onDragMove, contentDragger));
- draggable.on('dragEnd', _.bind(contentDragger.onDragEnd, contentDragger));
+ if ($(ele).data('droppable-class') !== droppableClass) {
+ console.log('populating dragable info for type ' + type);
+ $(ele).data('droppable-class', droppableClass);
+ $(ele).data('parent-location-selector', parentLocationSelector);
+ $(ele).data('child-selector', type);
+ var draggable = new Draggabilly(ele, {
+ handle: handleClass,
+ containment: '.wrapper-dnd'
+ });
+ draggable.on('dragStart', _.bind(contentDragger.onDragStart, contentDragger));
+ draggable.on('dragMove', _.bind(contentDragger.onDragMove, contentDragger));
+ draggable.on('dragEnd', _.bind(contentDragger.onDragEnd, contentDragger));
+ }
}
);
}
diff --git a/cms/static/js/views/baseview.js b/cms/static/js/views/baseview.js
index bf0d456bbe05..b89591c98794 100644
--- a/cms/static/js/views/baseview.js
+++ b/cms/static/js/views/baseview.js
@@ -16,6 +16,10 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b
"click .ui-toggle-expansion": "toggleExpandCollapse"
},
+ options: {
+ collapsedClass: 'collapsed'
+ },
+
//override the constructor function
constructor: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
@@ -48,7 +52,7 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b
// this element, e.g. clicking on the element of a child view container in a parent.
event.stopPropagation();
event.preventDefault();
- ViewUtils.toggleExpandCollapse(target);
+ ViewUtils.toggleExpandCollapse(target, this.options.collapsedClass);
},
/**
diff --git a/cms/static/js/views/course_outline.js b/cms/static/js/views/course_outline.js
index caa0d845bc1b..59a4fc319adf 100644
--- a/cms/static/js/views/course_outline.js
+++ b/cms/static/js/views/course_outline.js
@@ -9,14 +9,20 @@
* - adding units will automatically redirect to the unit page rather than showing them inline
*/
define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils",
- "js/models/xblock_outline_info"],
- function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo) {
+ "js/models/xblock_outline_info", "js/utils/drag_and_drop" ],
+ function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, ContentDragger) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
templateName: 'course-outline',
+ render: function() {
+ var renderResult = XBlockOutlineView.prototype.render.call(this);
+ this.makeContentDraggable(this.$el);
+ return renderResult;
+ },
+
shouldExpandChildren: function() {
// Expand the children if this xblock's locator is in the initially expanded state
if (this.initialState && _.contains(this.initialState.expanded_locators, this.model.id)) {
@@ -75,6 +81,7 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
viewState = viewState || {};
viewState.expanded_locators = expandedLocators.concat(viewState.expanded_locators || []);
view.initialState = viewState;
+ this.makeContentDraggable(view.$el);
return view.model.fetch({});
},
@@ -137,6 +144,36 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
expanded_locators: [ locator ],
scroll_offset: scrollOffset || 0
};
+ },
+
+ makeContentDraggable: function(el) {
+ if (el.hasClass("outline-section")) {
+ ContentDragger.makeDraggable(
+ '.outline-section',
+ '.section-drag-handle',
+ 'ol.list-sections',
+ 'article.outline',
+ el
+ );
+ }
+ else if (el.hasClass("outline-subsection")) {
+ ContentDragger.makeDraggable(
+ '.outline-subsection',
+ '.subsection-drag-handle',
+ 'ol.list-subsections',
+ 'li.outline-section',
+ el
+ );
+ }
+ else if (el.hasClass("outline-unit")) {
+ ContentDragger.makeDraggable(
+ '.outline-unit',
+ '.unit-drag-handle',
+ 'ol.list-units',
+ 'li.outline-subsection',
+ el
+ );
+ }
}
});
diff --git a/cms/static/js/views/overview.js b/cms/static/js/views/overview.js
index 115445b420d4..b94cf2286738 100644
--- a/cms/static/js/views/overview.js
+++ b/cms/static/js/views/overview.js
@@ -1,6 +1,6 @@
-define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification", "js/utils/drag_and_drop",
+define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/feedback_notification",
"js/utils/cancel_on_escape", "js/utils/get_date", "js/utils/module"],
- function (domReady, $, ui, _, gettext, NotificationView, ContentDragger, CancelOnEscape,
+ function (domReady, $, ui, _, gettext, NotificationView, CancelOnEscape,
DateUtils, ModuleUtils) {
var modalSelector = '.edit-section-publish-settings';
@@ -230,27 +230,6 @@ define(["domReady", "jquery", "jquery.ui", "underscore", "gettext", "js/views/fe
$('.new-courseware-section-button').bind('click', addNewSection);
$('.new-subsection-item').bind('click', addNewSubsection);
- // Section
- ContentDragger.makeDraggable(
- '.courseware-section',
- '.section-drag-handle',
- '.courseware-overview',
- 'article.courseware-overview'
- );
- // Subsection
- ContentDragger.makeDraggable(
- '.id-holder',
- '.subsection-drag-handle',
- '.subsection-list > ol',
- '.courseware-section'
- );
- // Unit
- ContentDragger.makeDraggable(
- '.unit',
- '.unit-drag-handle',
- 'ol.sortable-unit-list',
- 'li.courseware-subsection, article.subsection-body'
- );
});
return {
diff --git a/cms/static/js/views/pages/container.js b/cms/static/js/views/pages/container.js
index 25806b1fb1ab..c4a1f7e33ddf 100644
--- a/cms/static/js/views/pages/container.js
+++ b/cms/static/js/views/pages/container.js
@@ -12,6 +12,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
var XBlockContainerPage = BasePage.extend({
// takes XBlockInfo as a model
+ options: {
+ collapsedClass: 'is-collapsed'
+ },
+
view: 'container_preview',
initialize: function(options) {
diff --git a/cms/static/js/views/pages/container_subviews.js b/cms/static/js/views/pages/container_subviews.js
index 7bf27078a8d1..2ecd760568f1 100644
--- a/cms/static/js/views/pages/container_subviews.js
+++ b/cms/static/js/views/pages/container_subviews.js
@@ -108,7 +108,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
render: function () {
this.$el.html(this.template({
hasChanges: this.model.get('has_changes'),
- published: this.model.get('published'),
+ published: this.model.get('fully_published'),
editedOn: this.model.get('edited_on'),
editedBy: this.model.get('edited_by'),
publishedOn: this.model.get('published_on'),
@@ -138,7 +138,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
discardChanges: function (e) {
- var xblockInfo = this.model, that=this, renderPage = this.renderPage;
+ var xblockInfo = this.model, renderPage = this.renderPage;
if (e && e.preventDefault) {
e.preventDefault();
}
diff --git a/cms/static/js/views/pages/course_outline.js b/cms/static/js/views/pages/course_outline.js
index 95367b0626ed..4f0dacc83cdb 100644
--- a/cms/static/js/views/pages/course_outline.js
+++ b/cms/static/js/views/pages/course_outline.js
@@ -8,14 +8,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
// takes XBlockInfo as a model
events: {
- "click .toggle-button-expand-collapse": "toggleExpandCollapse"
+ "click .button-toggle-expand-collapse": "toggleExpandCollapse"
+ },
+
+ options: {
+ collapsedClass: 'is-collapsed'
},
initialize: function() {
var self = this;
this.initialState = this.options.initialState;
BasePage.prototype.initialize.call(this);
- this.$('.add-button').click(function(event) {
+ this.$('.button-new').click(function(event) {
self.outlineView.handleAddEvent(event);
});
this.model.on('change', this.setCollapseExpandVisibility, this);
@@ -23,19 +27,18 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
setCollapseExpandVisibility: function() {
var has_content = this.hasContent(),
- collapseExpandButton = $('.toggle-button-expand-collapse');
+ collapseExpandButton = $('.button-toggle-expand-collapse');
if (has_content) {
- collapseExpandButton.show();
+ collapseExpandButton.removeClass('is-hidden');
} else {
- collapseExpandButton.hide();
+ collapseExpandButton.addClass('is-hidden');
}
},
renderPage: function() {
- var locatorToShow;
this.setCollapseExpandVisibility();
this.outlineView = new CourseOutlineView({
- el: this.$('.course-outline'),
+ el: this.$('.outline'),
model: this.model,
isRoot: true,
initialState: this.initialState
@@ -50,20 +53,14 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
},
toggleExpandCollapse: function(event) {
- var toggleButton = this.$('.toggle-button-expand-collapse'),
+ var toggleButton = this.$('.button-toggle-expand-collapse'),
collapse = toggleButton.hasClass('collapse-all');
event.preventDefault();
toggleButton.toggleClass('collapse-all expand-all');
- this.$('.course-outline > ol > li').each(function(index, domElement) {
- var element = $(domElement),
- expandCollapseElement = element.find('.expand-collapse').first();
- if (collapse) {
- expandCollapseElement.removeClass('expand').addClass('collapse');
- element.addClass('collapsed');
- } else {
- expandCollapseElement.addClass('expand').removeClass('collapse');
- element.removeClass('collapsed');
- }
+ this.$('.list-sections > li').each(function(index, domElement) {
+ var element = $(domElement);
+ element.addClass(collapse ? 'is-collapsed' : 'is-expanded');
+ element.removeClass(collapse ? 'is-expanded' : 'is-collapsed');
});
}
});
diff --git a/cms/static/js/views/unit_outline.js b/cms/static/js/views/unit_outline.js
index 49366dadfe83..cea6b91b6825 100644
--- a/cms/static/js/views/unit_outline.js
+++ b/cms/static/js/views/unit_outline.js
@@ -23,7 +23,7 @@ define(['js/views/xblock_outline'],
previousAncestor = null;
if (this.model.get('ancestor_info')) {
ancestors = this.model.get('ancestor_info').ancestors;
- listElement = this.$('.sortable-list');
+ listElement = this.getListElement();
// Note: the ancestors are processed in reverse order because the tree wants to
// start at the root, but the ancestors are ordered by closeness to the unit,
// i.e. subsection and then section.
@@ -33,7 +33,7 @@ define(['js/views/xblock_outline'],
ancestorView.render();
listElement.append(ancestorView.$el);
previousAncestor = ancestor;
- listElement = ancestorView.$('.sortable-list');
+ listElement = ancestorView.getListElement();
}
}
return ancestorView;
diff --git a/cms/static/js/views/utils/view_utils.js b/cms/static/js/views/utils/view_utils.js
index f4f44126a82e..98eab24d961c 100644
--- a/cms/static/js/views/utils/view_utils.js
+++ b/cms/static/js/views/utils/view_utils.js
@@ -10,9 +10,13 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
/**
* Toggles the expanded state of the current element.
*/
- toggleExpandCollapse = function(target) {
+ toggleExpandCollapse = function(target, collapsedClass) {
+ // Support the old 'collapsed' option until fully switched over to is-collapsed
+ if (!collapsedClass) {
+ collapsedClass = 'collapsed';
+ }
target.closest('.expand-collapse').toggleClass('expand collapse');
- target.closest('.is-collapsible, .window').toggleClass('collapsed');
+ target.closest('.is-collapsible, .window').toggleClass(collapsedClass);
target.closest('.is-collapsible').children('article').slideToggle();
};
diff --git a/cms/static/js/views/xblock_outline.js b/cms/static/js/views/xblock_outline.js
index a34274327072..2b9218b60573 100644
--- a/cms/static/js/views/xblock_outline.js
+++ b/cms/static/js/views/xblock_outline.js
@@ -21,6 +21,10 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var XBlockOutlineView = BaseView.extend({
// takes XBlockInfo as a model
+ options: {
+ collapsedClass: 'is-collapsed is-expanded'
+ },
+
templateName: 'xblock-outline',
initialize: function() {
@@ -94,8 +98,12 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
this.renderedChildren = true;
},
+ getListElement: function() {
+ return this.$('> .outline-content > ol');
+ },
+
addChildView: function(childView) {
- this.$('> .sortable-list').append(childView.$el);
+ this.getListElement().append(childView.$el);
},
addNameEditor: function() {
@@ -136,7 +144,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
addButtonActions: function(element) {
var self = this;
element.find('.delete-button').click(_.bind(this.handleDeleteEvent, this));
- element.find('.add-button').click(_.bind(this.handleAddEvent, this));
+ element.find('.button-new').click(_.bind(this.handleAddEvent, this));
},
shouldRenderChildren: function() {
@@ -163,7 +171,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
xblockType = 'section';
} else if (category === 'sequential') {
xblockType = 'subsection';
- } else if (category === 'vertical' && parentInfo && parentInfo.get('category') === 'sequential') {
+ } else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) {
xblockType = 'unit';
}
return xblockType;
@@ -244,7 +252,20 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
XBlockViewUtils.addXBlock(target).done(function(locator) {
self.onChildAdded(locator, category, event);
});
- }
+ },
+
+ /**
+ * Override the default implementation to use the new is-collapsed/is-expanded classes.
+ * @param event
+ */
+ toggleExpandCollapse: function(event) {
+ var target = $(event.target);
+ // Don't propagate the event as it is possible that two views will both contain
+ // this element, e.g. clicking on the element of a child view container in a parent.
+ event.stopPropagation();
+ event.preventDefault();
+ ViewUtils.toggleExpandCollapse(target, this.options.collapsedClass);
+ },
});
return XBlockOutlineView;
diff --git a/cms/static/sass/_variables.scss b/cms/static/sass/_variables.scss
index 79384b6f4485..3376666f75b5 100644
--- a/cms/static/sass/_variables.scss
+++ b/cms/static/sass/_variables.scss
@@ -49,7 +49,6 @@ $gray-d3: shade($gray,60%);
$gray-d4: shade($gray,80%);
$blue: rgb(0, 159, 230);
-$blue-alt: rgb(88, 152, 219);
$blue-l1: tint($blue,20%);
$blue-l2: tint($blue,40%);
$blue-l3: tint($blue,60%);
@@ -105,7 +104,6 @@ $red-u2: desaturate($red,30%);
$red-u3: desaturate($red,45%);
$green: rgb(37, 184, 90);
-$green-alt: rgb(48, 183, 94);
$green-l1: tint($green,20%);
$green-l2: tint($green,40%);
$green-l3: tint($green,60%);
diff --git a/cms/static/sass/assets/_anims.scss b/cms/static/sass/assets/_anims.scss
index 5c12ab7daeb0..cf3f49e59bd2 100644
--- a/cms/static/sass/assets/_anims.scss
+++ b/cms/static/sass/assets/_anims.scss
@@ -248,6 +248,7 @@
@include animation(flashDouble $tmg-f1 ease-in-out 1);
}
+
// ====================
diff --git a/cms/static/sass/elements/_controls.scss b/cms/static/sass/elements/_controls.scss
index 163c128dfff2..c200c6e28715 100644
--- a/cms/static/sass/elements/_controls.scss
+++ b/cms/static/sass/elements/_controls.scss
@@ -1,8 +1,6 @@
// studio - elements - UI controls
// ====================
-// ====================
-
// general actions
%action {
@extend %ui-fake-link;
@@ -279,7 +277,7 @@
}
}
-// UI: elem is collapsible
+// UI: elem is collapsible - TODO: this should be transitioned away from in favor of %ui-expand-collapse
%expand-collapse {
@include transition(all $tmg-f2 linear 0s);
display: inline-block;
@@ -305,6 +303,41 @@
}
}
+// UI: expand collapse
+%ui-expand-collapse {
+ @include transition(all $tmg-f2 linear 0s);
+
+
+ // CASE: default (is expanded)
+ .ui-toggle-expansion {
+ @include transition(all $tmg-f2 ease-in-out 0s);
+ display: inline-block;
+ vertical-align: middle;
+
+ .icon {
+ @include transition(all $tmg-f2 ease-in-out 0s);
+ }
+
+ // STATE: hover/active
+ &:hover, &:active {
+ cursor: pointer;
+ color: $ui-link-color-focus;
+ }
+ }
+
+ // CASE: is collapsed
+ &.is-collapsed {
+
+ .ui-toggle-expansion {
+
+ .icon {
+ @include transform(rotate(-90deg));
+ @include transform-origin(50% 50%);
+ }
+ }
+ }
+}
+
// UI: drag handles
.drag-handle {
@@ -355,8 +388,8 @@
// UI: condition - valid drop
&.valid-drop {
- border-color: $blue-s1;
- box-shadow: 0 1px 2px 0 $blue-t2;
+ border-color: $ui-action-primary-color-focus;
+ box-shadow: 0 1px 2px 0 rgba($ui-action-primary-color-focus, 0.50);
}
}
@@ -382,3 +415,10 @@
}
}
}
+
+// UI: drop state - was dropped
+.was-dropped {
+ @include animation(pulse $tmg-avg ease-in-out 1);
+ border-color: $ui-action-primary-color-focus;
+ box-shadow: 0 1px 2px 0 rgba($ui-action-primary-color-focus, 0.50);
+}
diff --git a/cms/static/sass/elements/_layout.scss b/cms/static/sass/elements/_layout.scss
index 51731b890ff9..764af355521d 100644
--- a/cms/static/sass/elements/_layout.scss
+++ b/cms/static/sass/elements/_layout.scss
@@ -78,7 +78,8 @@
}
// CASE: new/create button
- &.new-button, &.button-new {
+ &.new-button,
+ &.button-new {
@extend %btn-primary-green;
@extend %sizing;
}
@@ -88,11 +89,6 @@
@extend %btn-secondary-gray;
@extend %sizing;
}
-
- // CASE: view live button
- &.button-view, &.view-button {
-
- }
}
}
}
diff --git a/cms/static/sass/elements/_modules.scss b/cms/static/sass/elements/_modules.scss
index bbdf1eaf1123..c0755b46935d 100644
--- a/cms/static/sass/elements/_modules.scss
+++ b/cms/static/sass/elements/_modules.scss
@@ -269,30 +269,27 @@ $outline-indent-width: $baseline;
// UI: containing elements on a course outline (currently sections/subsections)
%outline-container {
- // STATE: is-collapsed
- &.is-collapsed {
- border-left: ($baseline/4) solid $color-draft;
- }
+ @include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s);
+ border-left: ($baseline/4) solid $color-draft;
- // CASE: is published by not released
+ // CASE: is published but not released
&.is-published {
border-left-color: $color-published;
-
- &.is-published.is-released {
+ &.is-released {
border-left-color: $color-released;
}
}
- // CASE: has staff-only content
- &.has-staff-only {
+ // CASE: is presented for staff only
+ &.is-staff-only {
border-left-color: $color-staff-only;
}
- // CASE: has changes
- &.is-published.is-released.has-changes, // needed for poor specificity
- &.is-published.is-scheduled.has-changes, // needed for poor specificity
- &.has-changes {
+ // CASE: has unpublished content
+ &.is-published.is-released.has-unpublished-content, // needed for poor specificity
+ &.is-published.is-scheduled.has-unpublished-content, // needed for poor specificity
+ &.has-unpublished-content {
border-left-color: $color-warning;
}
@@ -317,7 +314,7 @@ $outline-indent-width: $baseline;
}
// CASE: has staff-only content
- &.has-staff-only {
+ &.is-staff-only {
// aint the prettiest, but needed to make sure direct children only
> .section-status,
@@ -330,10 +327,10 @@ $outline-indent-width: $baseline;
}
}
- // CASE: has changes
- &.is-published.is-released.has-changes, // needed for poor specificity
- &.is-published.is-scheduled.has-changes, // needed for poor specificity
- &.has-changes {
+ // CASE: has unpublished content
+ &.is-published.is-released.has-unpublished-content, // needed for poor specificity
+ &.is-published.is-scheduled.has-unpublished-content, // needed for poor specificity
+ &.has-unpublished-content {
// aint the prettiest, but needed to make sure direct children only
> .section-status .status-message,
@@ -365,9 +362,6 @@ $outline-indent-width: $baseline;
> .subsection-status .status-message,
> .unit-status .status-message {
- .icon {
- @include animation(pulse $tmg-s3 ease-in-out infinite);
- }
}
}
}
@@ -458,6 +452,7 @@ $outline-indent-width: $baseline;
@extend %outline-container;
margin-bottom: ($baseline/2);
border: 1px solid $gray-l4;
+ border-left: ($baseline/4) solid $color-draft;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: ($baseline*0.75);
@@ -469,7 +464,7 @@ $outline-indent-width: $baseline;
// STATE: is-collapsed
&.is-collapsed {
- border-left: ($baseline/4) solid $color-draft;
+
}
// header - title
diff --git a/cms/static/sass/views/_outline.scss b/cms/static/sass/views/_outline.scss
index a88eeaad1a7e..98994b7d0164 100644
--- a/cms/static/sass/views/_outline.scss
+++ b/cms/static/sass/views/_outline.scss
@@ -1,6 +1,21 @@
// studio - views - course outline
// ====================
+// view-specific utilities
+// --------------------
+%outline-item-header {
+ @include clearfix();
+ line-height: 0;
+}
+
+%outline-item-content-hidden {
+ display: none;
+}
+
+%outline-item-content-shown {
+ display: block;
+}
+
.view-outline {
// page structure
@@ -40,20 +55,30 @@
// page header
// --------------------
- .toggle-button-sections {
- @extend %t-copy-sub2;
- position: relative;
- display: none;
- float: right;
- margin-top: ($baseline/4);
- color: $gray-l1;
+ .button-toggle-expand-collapse {
- &.is-shown {
- display: block;
+ // STATE: action will collapse all
+ &.collapse-all {
+
+ .expand-all {
+ display: none;
+ }
+
+ .collapse-all {
+ display: block;
+ }
}
- .label {
- display: inline-block;
+ // STATE: action will expand all
+ &.expand-all {
+
+ .collapse-all {
+ display: none;
+ }
+
+ .expand-all {
+ display: block;
+ }
}
}
@@ -124,7 +149,7 @@
.add-item {
margin-top: ($baseline*0.75);
- .action-add {
+ .button-new {
@extend %ui-btn-flat-outline;
padding: ($baseline/2) $baseline;
display: block;
@@ -132,7 +157,7 @@
.icon {
display: inline-block;
vertical-align: middle;
- margin-right: ($baseline/4);
+ margin-right: ($baseline/2);
}
}
}
@@ -154,12 +179,13 @@
.outline-item {
// CASE: expand/collapse-able
- &.is-collapsible .expand-collapse {
+ &.is-collapsible {
- // STATE: hover/active
- &:hover, &:active {
+ // only select the current item's toggle expansion controls
+ &:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title {
- .item-title, .ui-toggle-expansion {
+ // STATE: hover/active
+ &:hover, &:active {
color: $blue;
}
}
@@ -181,6 +207,16 @@
}
}
}
+
+ // STATE: drag and drop
+ .drop-target-prepend .draggable-drop-indicator-initial {
+ opacity: 1.0;
+ }
+
+ // STATE: was dropped
+ &.was-dropped {
+ border-color: $blue;
+ }
}
// outline: sections
@@ -189,7 +225,7 @@
// header
.section-header {
- @include clearfix();
+ @extend %outline-item-header;
}
.section-header-details {
@@ -207,7 +243,6 @@
.wrapper-section-title {
width: flex-grid(5, 6);
- margin-top: -($baseline/4);
line-height: 0;
}
}
@@ -215,7 +250,7 @@
.section-header-actions {
float: right;
width: flex-grid(3, 9);
- margin-top: -($baseline/2);
+ margin-top: -($baseline/4);
text-align: right;
.actions-list {
@@ -228,6 +263,36 @@
.section-status {
margin: 0 0 0 $outline-indent-width;
}
+
+ // content
+ .section-content {
+ @extend %outline-item-content-shown;
+ }
+
+ // CASE: is-collapsible
+ &.is-collapsible {
+ @extend %ui-expand-collapse;
+
+ .ui-toggle-expansion {
+ @extend %t-icon3;
+ color: $gray-l3;
+ }
+ }
+
+ // STATE: is-expanded
+ &.is-expanded.is-expanded {
+ border-left-width: 1px;
+ border-left-color: $color-draft;
+ }
+
+ // STATE: is-collapsed
+ &.is-collapsed {
+ border-left-width: ($baseline/4);
+
+ .section-content {
+ @extend %outline-item-content-hidden;
+ }
+ }
}
// outline: subsections
@@ -240,7 +305,7 @@
// header
.subsection-header {
- @include clearfix();
+ @extend %outline-item-header;
}
.subsection-header-details {
@@ -280,6 +345,29 @@
.subsection-status {
margin: 0 0 0 $outline-indent-width;
}
+
+ // content
+ .subsection-content {
+ @extend %outline-item-content-shown;
+ }
+
+ // CASE: is-collapsible
+ &.is-collapsible {
+ @extend %ui-expand-collapse;
+
+ .ui-toggle-expansion {
+ @extend %t-icon4;
+ color: $gray-l3;
+ }
+ }
+
+ // STATE: is-collapsed
+ &.is-collapsed {
+
+ .subsection-content {
+ @extend %outline-item-content-hidden;
+ }
+ }
}
// outline: units
@@ -293,8 +381,7 @@
// header
.unit-header {
- @include clearfix();
- // margin-bottom: ($baseline/4);
+ @extend %outline-item-header;
}
.unit-header-details {
@@ -330,40 +417,17 @@
bottom: -13px;
left: 0;
}
-
- // CASE: DnD - empty subsection with unit dropping
- .drop-target-prepend .draggable-drop-indicator-initial {
- opacity: 1.0;
- }
-
- // STATE: was dropped
- &.was-dropped {
- background-color: $ui-update-color;
- }
}
// UI: drag and drop: subsection
.outline-subsection {
.draggable-drop-indicator-before {
- top: 0;
+ top: -6px;
}
.draggable-drop-indicator-after {
- bottom: 0;
- }
-
- // CASE: DnD - empty subsection with unit dropping
- .drop-target-prepend .draggable-drop-indicator-initial {
- opacity: 1.0;
- }
-
- // STATE: was dropped
- &.was-dropped {
-
- > .section-item {
- background-color: $ui-update-color !important; // nasty, but needed for specificity
- }
+ bottom: -7px;
}
}
@@ -371,19 +435,11 @@
.outline-unit {
.draggable-drop-indicator-before {
- top: 0;
+ top: -7px;
}
.draggable-drop-indicator-after {
- bottom: 0;
- }
-
- // STATE: was dropped
- &.was-dropped {
-
- > .section-item {
- background-color: $ui-update-color !important; // nasty, but needed for specificity
- }
+ bottom: -6px;
}
}
diff --git a/cms/templates/course_outline.html b/cms/templates/course_outline.html
index 41242b72290b..e01430921972 100644
--- a/cms/templates/course_outline.html
+++ b/cms/templates/course_outline.html
@@ -47,13 +47,13 @@