Commit 732bcbde by polesye Committed by Tim Babych

STUD-1911, STUD-1932: Publish sections, subsections and units from course outline.

parent 06842be8
...@@ -1197,7 +1197,7 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1197,7 +1197,7 @@ class ContentStoreTest(ContentStoreTestCase):
resp = self._show_course_overview(course.id) resp = self._show_course_overview(course.id)
self.assertContains( self.assertContains(
resp, resp,
'<article class="outline outline-course" data-locator="{locator}" data-course-key="{course_key}">'.format( '<article class="outline outline-complex outline-course" data-locator="{locator}" data-course-key="{course_key}">'.format(
locator='i4x://MITx/999/course/Robot_Super_Course', locator='i4x://MITx/999/course/Robot_Super_Course',
course_key='MITx/999/Robot_Super_Course', course_key='MITx/999/Robot_Super_Course',
), ),
......
...@@ -181,6 +181,7 @@ def xblock_handler(request, usage_key_string): ...@@ -181,6 +181,7 @@ def xblock_handler(request, usage_key_string):
content_type="text/plain" content_type="text/plain"
) )
# pylint: disable=unused-argument # pylint: disable=unused-argument
@require_http_methods(("GET")) @require_http_methods(("GET"))
@login_required @login_required
...@@ -449,7 +450,7 @@ def _create_item(request): ...@@ -449,7 +450,7 @@ def _create_item(request):
# if we add one then we need to also add it to the policy information (i.e. metadata) # if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs # we should remove this once we can break this reference from the course to static tabs
if category == 'static_tab': if category == 'static_tab':
display_name = display_name or _("Empty") # Prevent name being None display_name = display_name or _("Empty") # Prevent name being None
course = store.get_course(dest_usage_key.course_key) course = store.get_course(dest_usage_key.course_key)
course.tabs.append( course.tabs.append(
StaticTab( StaticTab(
...@@ -635,7 +636,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -635,7 +636,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
return None return None
is_xblock_unit = is_unit(xblock, parent_xblock) is_xblock_unit = is_unit(xblock, parent_xblock)
is_unit_with_changes = is_xblock_unit and modulestore().has_changes(xblock) has_changes = modulestore().has_changes(xblock)
if graders is None: if graders is None:
graders = CourseGradingModel.fetch(xblock.location.course_key).graders graders = CourseGradingModel.fetch(xblock.location.course_key).graders
...@@ -654,7 +655,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -654,7 +655,10 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
# Treat DEFAULT_START_DATE as a magic number that means the release date has not been set # 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 release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
visibility_state = _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None if xblock.category != 'course':
visibility_state = _compute_visibility_state(xblock, child_info, is_xblock_unit and has_changes)
else:
visibility_state = None
published = modulestore().compute_publish_state(xblock) != PublishState.private published = modulestore().compute_publish_state(xblock) != PublishState.private
xblock_info = { xblock_info = {
...@@ -664,7 +668,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -664,7 +668,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None, "edited_on": get_default_time_display(xblock.subtree_edited_on) if xblock.subtree_edited_on else None,
"published": published, "published": published,
"published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None, "published_on": get_default_time_display(xblock.published_date) if xblock.published_date else None,
'studio_url': xblock_studio_url(xblock, parent_xblock), "studio_url": xblock_studio_url(xblock, parent_xblock),
"released_to_students": datetime.now(UTC) > xblock.start, "released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date, "release_date": release_date,
"visibility_state": visibility_state, "visibility_state": visibility_state,
...@@ -675,6 +679,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -675,6 +679,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"due": xblock.fields['due'].to_json(xblock.due), "due": xblock.fields['due'].to_json(xblock.due),
"format": xblock.format, "format": xblock.format,
"course_graders": json.dumps([grader.get('type') for grader in graders]), "course_graders": json.dumps([grader.get('type') for grader in graders]),
"has_changes": has_changes,
} }
if data is not None: if data is not None:
xblock_info["data"] = data xblock_info["data"] = data
...@@ -689,14 +694,13 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F ...@@ -689,14 +694,13 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
else: else:
xblock_info["ancestor_has_staff_lock"] = False xblock_info["ancestor_has_staff_lock"] = False
# Currently, 'edited_by', 'published_by', and 'release_date_from', and 'has_changes' are only used by the # Currently, 'edited_by', 'published_by', and 'release_date_from' are only used by the
# container page when rendering a unit. Since they are expensive to compute, only include them for units # container page when rendering a unit. Since they are expensive to compute, only include them for units
# that are not being rendered on the course outline. # that are not being rendered on the course outline.
if is_xblock_unit and not course_outline: if is_xblock_unit and not course_outline:
xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_by) xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_by)
xblock_info["published_by"] = safe_get_username(xblock.published_by) xblock_info["published_by"] = safe_get_username(xblock.published_by)
xblock_info["currently_visible_to_students"] = is_currently_visible_to_students(xblock) xblock_info["currently_visible_to_students"] = is_currently_visible_to_students(xblock)
xblock_info['has_changes'] = is_unit_with_changes
if release_date: if release_date:
xblock_info["release_date_from"] = _get_release_date_from(xblock) xblock_info["release_date_from"] = _get_release_date_from(xblock)
if visibility_state == VisibilityState.staff_only: if visibility_state == VisibilityState.staff_only:
......
...@@ -59,7 +59,7 @@ function(Backbone, _, str, ModuleUtils) { ...@@ -59,7 +59,7 @@ function(Backbone, _, str, ModuleUtils) {
*/ */
"visibility_state": null, "visibility_state": null,
/** /**
* True iff the release date of the xblock is in the past. * True if the release date of the xblock is in the past.
*/ */
'released_to_students': null, 'released_to_students': null,
/** /**
...@@ -153,6 +153,10 @@ function(Backbone, _, str, ModuleUtils) { ...@@ -153,6 +153,10 @@ function(Backbone, _, str, ModuleUtils) {
return childInfo && childInfo.children.length > 0; return childInfo && childInfo.children.length > 0;
}, },
isPublishable: function(){
return !this.get('published') || this.get('has_changes');
},
/** /**
* Return a list of convenience methods to check affiliation to the category. * Return a list of convenience methods to check affiliation to the category.
* @return {Array} * @return {Array}
......
...@@ -5,14 +5,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -5,14 +5,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
describe("CourseOutlinePage", function() { describe("CourseOutlinePage", function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests, var createCourseOutlinePage, displayNameInput, model, outlinePage, requests,
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState, getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore'); mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
createMockCourseJSON = function(id, displayName, children) { createMockCourseJSON = function(options, children) {
return { return $.extend(true, {}, {
id: id, id: 'mock-course',
display_name: displayName, display_name: 'Mock Course',
category: 'course', category: 'course',
studio_url: '/course/slashes:MockCourse', studio_url: '/course/slashes:MockCourse',
is_container: true, is_container: true,
...@@ -22,17 +22,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -22,17 +22,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
edited_by: 'MockUser', edited_by: 'MockUser',
has_explicit_staff_lock: false, has_explicit_staff_lock: false,
child_info: { child_info: {
display_name: 'Section',
category: 'chapter', category: 'chapter',
children: children display_name: 'Section',
children: []
} }
}; }, options, {child_info: {children: children}});
}; };
createMockSectionJSON = function(id, displayName, children) {
return { createMockSectionJSON = function(options, children) {
id: id, return $.extend(true, {}, {
id: 'mock-section',
display_name: 'Mock Section',
category: 'chapter', category: 'chapter',
display_name: displayName,
studio_url: '/course/slashes:MockCourse', studio_url: '/course/slashes:MockCourse',
is_container: true, is_container: true,
has_changes: false, has_changes: false,
...@@ -43,14 +44,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -43,14 +44,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
child_info: { child_info: {
category: 'sequential', category: 'sequential',
display_name: 'Subsection', display_name: 'Subsection',
children: children children: []
} }
}; }, options, {child_info: {children: children}});
}; };
createMockSubsectionJSON = function(id, displayName, children) {
return { createMockSubsectionJSON = function(options, children) {
id: id, return $.extend(true, {}, {
display_name: displayName, id: 'mock-subsection',
display_name: 'Mock Subsection',
category: 'sequential', category: 'sequential',
studio_url: '/course/slashes:MockCourse', studio_url: '/course/slashes:MockCourse',
is_container: true, is_container: true,
...@@ -63,9 +65,24 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -63,9 +65,24 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
child_info: { child_info: {
category: 'vertical', category: 'vertical',
display_name: 'Unit', display_name: 'Unit',
children: children children: []
} }
}; }, options, {child_info: {children: children}});
};
createMockVerticalJSON = function(options) {
return $.extend(true, {}, {
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: false,
published: true,
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}, options);
}; };
getItemsOfType = function(type) { getItemsOfType = function(type) {
...@@ -108,40 +125,100 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -108,40 +125,100 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
return outlinePage; return outlinePage;
}; };
verifyTypePublishable = function (type, getMockCourseJSON) {
var createCourseOutlinePageAndShowUnit, verifyPublishButton;
createCourseOutlinePageAndShowUnit = function (test, courseJSON, createOnly) {
outlinePage = createCourseOutlinePage.apply(this, arguments);
if (type === 'unit') {
expandItemsAndVerifyState('subsection');
}
};
verifyPublishButton = function (test, courseJSON, createOnly) {
createCourseOutlinePageAndShowUnit.apply(this, arguments);
expect(getItemHeaders(type).find('.publish-button')).toExist();
};
it('can be published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
getItemHeaders(type).find('.publish-button').click();
$(".wrapper-modal-window .action-publish").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-' + type, {
publish : 'make_public'
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
create_sinon.respondWithJson(requests, {});
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section');
});
it('should show publish button if it is not published and not changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is published and changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: true
});
verifyPublishButton(this, mockCourseJSON);
});
it('should show publish button if it is not published, but changed', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: true,
published: false
});
verifyPublishButton(this, mockCourseJSON);
});
it('should hide publish button if it is not changed, but published', function() {
var mockCourseJSON = getMockCourseJSON({
has_changes: false,
published: true
});
createCourseOutlinePageAndShowUnit(this, mockCourseJSON);
expect(getItemHeaders(type).find('.publish-button')).not.toExist();
});
};
beforeEach(function () { beforeEach(function () {
view_helpers.installMockAnalytics(); view_helpers.installMockAnalytics();
view_helpers.installViewTemplates(); view_helpers.installViewTemplates();
view_helpers.installTemplate('course-outline'); view_helpers.installTemplates([
view_helpers.installTemplate('xblock-string-field-editor'); 'course-outline', 'xblock-string-field-editor', 'modal-button',
view_helpers.installTemplate('modal-button'); 'basic-modal', 'course-outline-modal', 'release-date-editor',
view_helpers.installTemplate('basic-modal'); 'due-date-editor', 'grading-editor', 'publish-editor',
view_helpers.installTemplate('edit-outline-item-modal'); 'staff-lock-editor'
]);
appendSetFixtures(mockOutlinePage); appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [ mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON('mock-section', 'Mock Section', [ createMockSectionJSON({}, [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{ createMockSubsectionJSON({}, [
id: 'mock-unit', createMockVerticalJSON()
display_name: 'Mock Unit', ])
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: false,
published: true,
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}])
]) ])
]); ]);
mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []); mockEmptyCourseJSON = createMockCourseJSON();
mockSingleSectionCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [ mockSingleSectionCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON('mock-section', 'Mock Section', []) createMockSectionJSON()
]); ]);
}); });
afterEach(function () { afterEach(function () {
view_helpers.removeMockAnalytics(); view_helpers.removeMockAnalytics();
edit_helpers.cancelModalIfShowing(); edit_helpers.cancelModalIfShowing();
// Clean up after the $.datepicker
$("#start_date").datepicker( "destroy" );
$("#due_date").datepicker( "destroy" );
$('.ui-datepicker').remove();
}); });
describe('Initial display', function() { describe('Initial display', function() {
...@@ -201,7 +278,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -201,7 +278,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// Expect the UI to just fetch the new section and repaint it // Expect the UI to just fetch the new section and repaint it
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2'); create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
create_sinon.respondWithJson(requests, create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section-2', 'Mock Section 2', [])); createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}));
sectionElements = getItemsOfType('section'); sectionElements = getItemsOfType('section');
expect(sectionElements.length).toBe(2); expect(sectionElements.length).toBe(2);
expect($(sectionElements[0]).data('locator')).toEqual('mock-section'); expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
...@@ -269,9 +346,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -269,9 +346,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be deleted', function() { it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy(), requestCount; var promptSpy = view_helpers.createPromptSpy(), requestCount;
createCourseOutlinePage(this, createMockCourseJSON('mock-course', 'Mock Course', [ createCourseOutlinePage(this, createMockCourseJSON({}, [
createMockSectionJSON('mock-section', 'Mock Section', []), createMockSectionJSON(),
createMockSectionJSON('mock-section-2', 'Mock Section 2', []) createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
])); ]));
getItemHeaders('section').find('.delete-button').first().click(); getItemHeaders('section').find('.delete-button').first().click();
view_helpers.confirmPrompt(promptSpy); view_helpers.confirmPrompt(promptSpy);
...@@ -356,7 +433,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -356,7 +433,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
outlinePage.$('.section-header-actions .configure-button').click(); outlinePage.$('.section-header-actions .configure-button').click();
$("#start_date").val("1/2/2015"); $("#start_date").val("1/2/2015");
// Section release date can't be cleared. // Section release date can't be cleared.
expect($(".edit-outline-item-modal .action-clear")).not.toExist(); expect($(".wrapper-modal-window .action-clear")).not.toExist();
// Section does not contain due_date or grading type selector // Section does not contain due_date or grading type selector
expect($("due_date")).not.toExist(); expect($("due_date")).not.toExist();
...@@ -364,9 +441,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -364,9 +441,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// Staff lock controls are always visible // Staff lock controls are always visible
expect($("#staff_lock")).toExist(); expect($("#staff_lock")).toExist();
$(".wrapper-modal-window .action-save").click();
$(".edit-outline-item-modal .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', { create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
"metadata":{ "metadata":{
"start":"2015-01-02T00:00:00.000Z" "start":"2015-01-02T00:00:00.000Z"
...@@ -376,25 +451,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -376,25 +451,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// This is the response for the change operation. // This is the response for the change operation.
create_sinon.respondWithJson(requests, {}); create_sinon.respondWithJson(requests, {});
var mockResponseSectionJSON = $.extend(true, {}, var mockResponseSectionJSON = createMockSectionJSON({
createMockSectionJSON('mock-section', 'Mock Section', [ release_date: 'Jan 02, 2015 at 00:00 UTC'
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{ }, [
id: 'mock-unit', createMockSubsectionJSON({}, [
display_name: 'Mock Unit', createMockVerticalJSON({
category: 'vertical', has_changes: true,
studio_url: '/container/mock-unit', published: false
is_container: true, })
has_changes: true,
published: false,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}
]) ])
]), ]);
{
release_date: 'Jan 02, 2015 at 00:00 UTC',
}
);
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section') create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section')
expect(requests.length).toBe(2); expect(requests.length).toBe(2);
// This is the response for the subsequent fetch operation for the section. // This is the response for the subsequent fetch operation for the section.
...@@ -402,6 +468,46 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -402,6 +468,46 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($(".outline-section .status-release-value")).toContainText("Jan 02, 2015 at 00:00 UTC"); expect($(".outline-section .status-release-value")).toContainText("Jan 02, 2015 at 00:00 UTC");
}); });
verifyTypePublishable('section', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON(options, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished subsections and units', function () {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: 'Unit 100'}),
createMockVerticalJSON({published: false, display_name: 'Unit 50'})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true, display_name: 'Unit 1'})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
]),
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true}),
])
])
]), modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('section').first().find('.publish-button').click();
modalWindow = $('.wrapper-modal-window');
expect(modalWindow.find('.outline-unit').length).toBe(3);
expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual(
['Unit 100', 'Unit 50', 'Unit 1']
)
expect(modalWindow.find('.outline-subsection').length).toBe(2);
});
}); });
describe("Subsection", function() { describe("Subsection", function() {
...@@ -419,37 +525,25 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -419,37 +525,25 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
}; };
// Contains hard-coded dates because dates are presented in different formats. // Contains hard-coded dates because dates are presented in different formats.
mockServerValuesJson = $.extend(true, {}, var mockServerValuesJson = createMockSectionJSON({
createMockSectionJSON('mock-section', 'Mock Section', [ release_date: 'Jan 01, 2970 at 05:00 UTC'
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{ }, [
id: 'mock-unit', createMockSubsectionJSON({
display_name: 'Mock Unit', graded: true,
category: 'vertical', due_date: 'Jul 10, 2014 at 00:00 UTC',
studio_url: '/container/mock-unit', release_date: 'Jul 09, 2014 at 00:00 UTC',
is_container: true, start: "2014-07-09T00:00:00Z",
has_changes: true, format: "Lab",
published: false, due: "2014-07-10T00:00:00Z",
edited_on: 'Jul 02, 2014 at 20:56 UTC', has_explicit_staff_lock: true,
edited_by: 'MockUser' staff_only_message: true
} }, [
createMockVerticalJSON({
has_changes: true,
published: false
})
]) ])
]), ]);
{
release_date: 'Jan 01, 2970 at 05:00 UTC',
child_info: { //Section child_info
children: [{ // Section children
graded: true,
due_date: 'Jul 10, 2014 at 00:00 UTC',
release_date: 'Jul 09, 2014 at 00:00 UTC',
start: "2014-07-09T00:00:00Z",
format: "Lab",
due: "2014-07-10T00:00:00Z",
has_explicit_staff_lock: true,
staff_only_message: true
}]
}
}
);
it('can be deleted', function() { it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy(); var promptSpy = view_helpers.createPromptSpy();
...@@ -492,9 +586,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -492,9 +586,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.respondWithJson(requests, { }); create_sinon.respondWithJson(requests, { });
// This is the response for the subsequent fetch operation for the section. // This is the response for the subsequent fetch operation for the section.
create_sinon.respondWithJson(requests, create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section', 'Mock Section', [ createMockSectionJSON({}, [
createMockSubsectionJSON('mock-subsection', updatedDisplayName, []) createMockSubsectionJSON({
])); display_name: updatedDisplayName
})
])
);
// Find the display name again in the refreshed DOM and verify it // Find the display name again in the refreshed DOM and verify it
displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field'); displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName); view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
...@@ -514,7 +611,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -514,7 +611,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click(); outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true); setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
$(".edit-outline-item-modal .action-save").click(); $(".wrapper-modal-window .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', { create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
"graderType":"Lab", "graderType":"Lab",
"publish": "republish", "publish": "republish",
...@@ -550,7 +647,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -550,7 +647,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createCourseOutlinePage(this, mockCourseJSON, false); createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-item .outline-subsection .configure-button').click(); outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true); setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
$(".edit-outline-item-modal .action-save").click(); $(".wrapper-modal-window .action-save").click();
// This is the response for the change operation. // This is the response for the change operation.
create_sinon.respondWithJson(requests, {}); create_sinon.respondWithJson(requests, {});
...@@ -568,29 +665,62 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -568,29 +665,62 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($("#grading_type").val()).toBe('Lab'); expect($("#grading_type").val()).toBe('Lab');
expect($("#staff_lock").is(":checked")).toBe(true); expect($("#staff_lock").is(":checked")).toBe(true);
$(".edit-outline-item-modal .scheduled-date-input .action-clear").click(); $(".wrapper-modal-window .scheduled-date-input .action-clear").click();
$(".edit-outline-item-modal .due-date-input .action-clear").click(); $(".wrapper-modal-window .due-date-input .action-clear").click();
expect($("#start_date").val()).toBe(''); expect($("#start_date").val()).toBe('');
expect($("#due_date").val()).toBe(''); expect($("#due_date").val()).toBe('');
$("#grading_type").val('notgraded'); $("#grading_type").val('notgraded');
$("#staff_lock").prop('checked', false); $("#staff_lock").prop('checked', false);
$(".edit-outline-item-modal .action-save").click(); $(".wrapper-modal-window .action-save").click();
// This is the response for the change operation. // This is the response for the change operation.
create_sinon.respondWithJson(requests, {}); create_sinon.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation. // This is the response for the subsequent fetch operation.
create_sinon.respondWithJson(requests, create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section', 'Mock Section', [ createMockSectionJSON({}, [createMockSubsectionJSON()])
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [])
])
); );
expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC"); expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-date")).not.toExist(); expect($(".outline-subsection .status-grading-date")).not.toExist();
expect($(".outline-subsection .status-grading-value")).not.toExist(); expect($(".outline-subsection .status-grading-value")).not.toExist();
expect($(".outline-subsection .status-message-copy")).not.toContainText("Contains staff only content"); expect($(".outline-subsection .status-message-copy")).not.toContainText("Contains staff only content");
}); });
verifyTypePublishable('subsection', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON(options, [
createMockVerticalJSON()
])
])
]);
});
it('can display a publish modal with a list of unpublished units', function () {
var mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({has_changes: true}, [
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON(),
createMockVerticalJSON({has_changes: true, display_name: "Unit 100"}),
createMockVerticalJSON({published: false, display_name: "Unit 50"})
]),
createMockSubsectionJSON({has_changes: true}, [
createMockVerticalJSON({has_changes: true})
]),
createMockSubsectionJSON({}, [createMockVerticalJSON])
])
]), modalWindow;
createCourseOutlinePage(this, mockCourseJSON, false);
getItemHeaders('subsection').first().find('.publish-button').click();
modalWindow = $('.wrapper-modal-window');
expect(modalWindow.find('.outline-unit').length).toBe(2);
expect(_.compact(_.map(modalWindow.find('.outline-unit').text().split("\n"), $.trim))).toEqual(
['Unit 100', 'Unit 50']
)
expect(modalWindow.find('.outline-subsection')).not.toExist();
});
}); });
// Note: most tests for units can be found in Bok Choy // Note: most tests for units can be found in Bok Choy
...@@ -615,6 +745,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers" ...@@ -615,6 +745,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
unitAnchor = getItemsOfType('unit').find('.unit-title a'); unitAnchor = getItemsOfType('unit').find('.unit-title a');
expect(unitAnchor.attr('href')).toBe('/container/mock-unit'); expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
}); });
verifyTypePublishable('unit', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
});
}); });
describe("Date and Time picker", function() { describe("Date and Time picker", function() {
......
...@@ -8,9 +8,12 @@ ...@@ -8,9 +8,12 @@
* - changes cause a refresh of the entire section rather than just the view for the changed xblock * - changes cause a refresh of the entire section rather than just the view for the changed xblock
* - adding units will automatically redirect to the unit page rather than showing them inline * - 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", define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils", "js/views/utils/xblock_utils",
"js/models/xblock_outline_info", "js/views/modals/edit_outline_item", "js/utils/drag_and_drop"], "js/models/xblock_outline_info", "js/views/modals/course_outline_modals", "js/utils/drag_and_drop"],
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, EditSectionXBlockModal, ContentDragger) { function(
$, _, XBlockOutlineView, ViewUtils, XBlockViewUtils,
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger
) {
var CourseOutlineView = XBlockOutlineView.extend({ var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model // takes XBlockOutlineInfo as a model
...@@ -144,13 +147,30 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ ...@@ -144,13 +147,30 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
}, },
editXBlock: function() { editXBlock: function() {
var modal = new EditSectionXBlockModal({ var modal = CourseOutlineModalsFactory.getModal('edit', this.model, {
model: this.model,
onSave: this.refresh.bind(this), onSave: this.refresh.bind(this),
parentInfo: this.parentInfo parentInfo: this.parentInfo,
xblockType: XBlockViewUtils.getXBlockType(
this.model.get('category'), this.parentView.model, true
)
}); });
modal.show(); if (modal) {
modal.show();
}
},
publishXBlock: function() {
var modal = CourseOutlineModalsFactory.getModal('publish', this.model, {
onSave: this.refresh.bind(this),
xblockType: XBlockViewUtils.getXBlockType(
this.model.get('category'), this.parentView.model, true
)
});
if (modal) {
modal.show();
}
}, },
addButtonActions: function(element) { addButtonActions: function(element) {
...@@ -159,6 +179,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_ ...@@ -159,6 +179,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
event.preventDefault(); event.preventDefault();
this.editXBlock(); this.editXBlock();
}.bind(this)); }.bind(this));
element.find('.publish-button').click(function(event) {
event.preventDefault();
this.publishXBlock();
}.bind(this));
}, },
makeContentDraggable: function(element) { makeContentDraggable: function(element) {
......
/**
* The CourseOutlineXBlockModal is a Backbone view that shows an editor in a modal window.
* It has nested views: for release date, due date and grading format.
* It is invoked using the editXBlock method and uses xblock_info as a model,
* and upon save parent invokes refresh function that fetches updated model and
* re-renders edited course outline.
*/
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/baseview',
'js/views/modals/base_modal', 'date', 'js/views/utils/xblock_utils',
'js/utils/date_utils'
], function(
$, Backbone, _, gettext, BaseView, BaseModal, date, XBlockViewUtils, DateUtils
) {
'use strict';
var CourseOutlineXBlockModal, SettingsXBlockModal, PublishXBlockModal, AbstractEditor, BaseDateEditor,
ReleaseDateEditor, DueDateEditor, GradingEditor, PublishEditor, StaffLockEditor;
CourseOutlineXBlockModal = BaseModal.extend({
events : {
'click .action-save': 'save'
},
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'course-outline',
modalType: 'edit-settings',
addSaveButton: true,
modalSize: 'med',
viewSpecificClasses: 'confirm',
editors: []
}),
initialize: function() {
BaseModal.prototype.initialize.call(this);
this.events = $.extend({}, BaseModal.prototype.events, this.events);
this.template = this.loadTemplate('course-outline-modal');
this.options.title = this.getTitle();
},
afterRender: function () {
BaseModal.prototype.afterRender.call(this);
this.initializeEditors();
},
initializeEditors: function () {
this.options.editors = _.map(this.options.editors, function (Editor) {
return new Editor({
parentElement: this.$('.modal-section'),
model: this.model,
xblockType: this.options.xblockType
});
}, this);
},
getTitle: function () {
return '';
},
getIntroductionMessage: function () {
return '';
},
getContentHtml: function() {
return this.template(this.getContext());
},
save: function(event) {
event.preventDefault();
var requestData = this.getRequestData();
if (!_.isEqual(requestData, { metadata: {} })) {
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
success: this.options.onSave
});
}
this.hide();
},
/**
* Return context for the modal.
* @return {Object}
*/
getContext: function () {
return $.extend({
xblockInfo: this.model,
introductionMessage: this.getIntroductionMessage()
});
},
/**
* Return request data.
* @return {Object}
*/
getRequestData: function () {
var requestData = _.map(this.options.editors, function (editor) {
return editor.getRequestData();
});
return $.extend.apply(this, [true, {}].concat(requestData));
}
});
SettingsXBlockModal = CourseOutlineXBlockModal.extend({
getTitle: function () {
return interpolate(
gettext('%(display_name)s Settings'),
{ display_name: this.model.get('display_name') }, true
);
},
getIntroductionMessage: function () {
return interpolate(
gettext('Change the settings for %(display_name)s'),
{ display_name: this.model.get('display_name') }, true
);
}
});
PublishXBlockModal = CourseOutlineXBlockModal.extend({
events : {
'click .action-publish': 'save'
},
initialize: function() {
CourseOutlineXBlockModal.prototype.initialize.call(this);
if (this.options.xblockType) {
this.options.modalName = 'bulkpublish-' + this.options.xblockType;
}
},
getTitle: function () {
return interpolate(
gettext('Publish %(display_name)s'),
{ display_name: this.model.get('display_name') }, true
);
},
getIntroductionMessage: function () {
return interpolate(
gettext('Publish all unpublished changes for this %(item)s?'),
{ item: this.options.xblockType }, true
);
},
addActionButtons: function() {
this.addActionButton('publish', gettext('Publish'), true);
this.addActionButton('cancel', gettext('Cancel'));
}
});
AbstractEditor = BaseView.extend({
tagName: 'section',
templateName: null,
initialize: function() {
this.template = this.loadTemplate(this.templateName);
this.parentElement = this.options.parentElement;
this.render();
},
render: function () {
var html = this.template($.extend({}, {
xblockInfo: this.model,
xblockType: this.options.xblockType
}, this.getContext()));
this.$el.html(html);
this.parentElement.append(this.$el);
},
getContext: function () {
return {};
},
getRequestData: function () {
return {};
}
});
BaseDateEditor = AbstractEditor.extend({
// Attribute name in the model, should be defined in children classes.
fieldName: null,
events : {
'click .clear-date': 'clearValue'
},
afterRender: function () {
AbstractEditor.prototype.afterRender.call(this);
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
this.$('input.time').timepicker({
'timeFormat' : 'H:i',
'forceRoundTime': true
});
if (this.model.get(this.fieldName)) {
DateUtils.setDate(
this.$('input.date'), this.$('input.time'),
this.model.get(this.fieldName)
);
}
}
});
DueDateEditor = BaseDateEditor.extend({
fieldName: 'due',
templateName: 'due-date-editor',
className: 'modal-section-content has-actions due-date-input grading-due-date',
getValue: function () {
return DateUtils.getDate(this.$('#due_date'), this.$('#due_time'));
},
clearValue: function (event) {
event.preventDefault();
this.$('#due_time, #due_date').val('');
},
getRequestData: function () {
return {
metadata: {
'due': this.getValue()
}
};
}
});
ReleaseDateEditor = BaseDateEditor.extend({
fieldName: 'start',
templateName: 'release-date-editor',
className: 'edit-settings-release scheduled-date-input',
startingReleaseDate: null,
afterRender: function () {
BaseDateEditor.prototype.afterRender.call(this);
// Store the starting date and time so that we can determine if the user
// actually changed it when "Save" is pressed.
this.startingReleaseDate = this.getValue();
},
getValue: function () {
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
},
clearValue: function (event) {
event.preventDefault();
this.$('#start_time, #start_date').val('');
},
getRequestData: function () {
var newReleaseDate = this.getValue();
if (JSON.stringify(newReleaseDate) === JSON.stringify(this.startingReleaseDate)) {
return {};
}
return {
metadata: {
'start': newReleaseDate
}
};
}
});
GradingEditor = AbstractEditor.extend({
templateName: 'grading-editor',
className: 'edit-settings-grading',
afterRender: function () {
AbstractEditor.prototype.afterRender.call(this);
this.setValue(this.model.get('format'));
},
setValue: function (value) {
this.$('#grading_type').val(value);
},
getValue: function () {
return this.$('#grading_type').val();
},
getRequestData: function () {
return {
'graderType': this.getValue()
};
},
getContext: function () {
return {
graderTypes: JSON.parse(this.model.get('course_graders'))
};
}
});
PublishEditor = AbstractEditor.extend({
templateName: 'publish-editor',
className: 'edit-settings-publish',
getRequestData: function () {
return {
publish: 'make_public'
};
}
});
StaffLockEditor = AbstractEditor.extend({
templateName: 'staff-lock-editor',
className: 'edit-staff-lock',
isModelLocked: function() {
return this.model.get('has_explicit_staff_lock');
},
isAncestorLocked: function() {
return this.model.get('ancestor_has_staff_lock');
},
afterRender: function () {
AbstractEditor.prototype.afterRender.call(this);
this.setLock(this.isModelLocked());
},
setLock: function(value) {
this.$('#staff_lock').prop('checked', value);
},
isLocked: function() {
return this.$('#staff_lock').is(':checked');
},
hasChanges: function() {
return this.isModelLocked() != this.isLocked();
},
getRequestData: function() {
return this.hasChanges() ? {
publish: 'republish',
metadata: {
visible_to_staff_only: this.isLocked() ? true : null
}
} : {};
},
getContext: function () {
return {
hasExplicitStaffLock: this.isModelLocked(),
ancestorLocked: this.isAncestorLocked()
}
}
});
return {
getModal: function (type, xblockInfo, options) {
if (type === 'edit') {
return this.getEditModal(xblockInfo, options);
} else if (type === 'publish') {
return this.getPublishModal(xblockInfo, options);
}
},
getEditModal: function (xblockInfo, options) {
var editors = [];
if (xblockInfo.isChapter()) {
editors = [ReleaseDateEditor, StaffLockEditor];
} else if (xblockInfo.isSequential()) {
editors = [ReleaseDateEditor, GradingEditor, DueDateEditor, StaffLockEditor];
} else if (xblockInfo.isVertical()) {
editors = [StaffLockEditor];
}
return new SettingsXBlockModal($.extend({
editors: editors,
model: xblockInfo
}, options));
},
getPublishModal: function (xblockInfo, options) {
return new PublishXBlockModal($.extend({
editors: [PublishEditor],
model: xblockInfo
}, options));
}
};
});
/**
* The EditSectionXBlockModal is a Backbone view that shows an editor in a modal window.
* It has nested views: for release date, due date, grading format, and staff lock.
* It is invoked using the editXBlock method and uses xblock_info as a model,
* and upon save parent invokes refresh function that fetches updated model and
* re-renders edited course outline.
*/
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_modal',
'date', 'js/views/utils/xblock_utils', 'js/utils/date_utils', 'js/views/utils/view_utils'
],
function(
$, Backbone, _, gettext, BaseModal, date, XBlockViewUtils, DateUtils, ViewUtils
) {
'use strict';
var EditSectionXBlockModal, BaseDateView, ReleaseDateView, DueDateView,
GradingView, StaffLockView;
EditSectionXBlockModal = BaseModal.extend({
events : {
'click .action-save': 'save',
'click .action-modes a': 'changeMode'
},
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'edit-outline-item',
modalType: 'edit-settings',
addSaveButton: true,
modalSize: 'med',
viewSpecificClasses: 'confirm'
}),
initialize: function() {
BaseModal.prototype.initialize.call(this);
this.events = _.extend({}, BaseModal.prototype.events, this.events);
this.template = this.loadTemplate('edit-outline-item-modal');
this.options.title = this.getTitle();
this.initializeComponents();
},
getTitle: function () {
return _.template(
gettext('<%= sectionName %> Settings'),
{ sectionName: this.model.get('display_name') }
);
},
getContentHtml: function() {
return this.template(this.getContext());
},
afterRender: function() {
BaseModal.prototype.render.apply(this, arguments);
this.invokeComponentMethod('afterRender');
},
save: function(event) {
event.preventDefault();
var requestData = _.extend({}, this.getRequestData(), {
metadata: this.getMetadata()
});
// Only update if something changed to prevent items from erroneously entering draft state
if (!_.isEqual(requestData, { metadata: {} })) {
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
success: this.options.onSave
});
}
this.hide();
},
/**
* Call the method on each value in the list. If the element of the
* list doesn't have such a method it will be skipped.
* @param {String} methodName The method name needs to be called.
* @return {Object}
*/
invokeComponentMethod: function (methodName) {
var values = _.map(this.components, function (component) {
if (_.isFunction(component[methodName])) {
return component[methodName].call(component);
}
});
return _.extend.apply(this, [{}].concat(values));
},
/**
* Return context for the modal.
* @return {Object}
*/
getContext: function () {
return _.extend({
xblockInfo: this.model,
xblockType: XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo, true)
}, this.invokeComponentMethod('getContext'));
},
/**
* Return request data.
* @return {Object}
*/
getRequestData: function () {
return this.invokeComponentMethod('getRequestData');
},
/**
* Return metadata for the XBlock.
* @return {Object}
*/
getMetadata: function () {
return this.invokeComponentMethod('getMetadata');
},
/**
* Initialize internal components.
*/
initializeComponents: function () {
this.components = [];
this.components.push(
new StaffLockView({
selector: '.edit-staff-lock',
parentView: this,
model: this.model
})
);
if (this.model.isChapter() || this.model.isSequential()) {
this.components.push(
new ReleaseDateView({
selector: '.scheduled-date-input',
parentView: this,
model: this.model
})
);
}
if (this.model.isSequential()) {
this.components.push(
new DueDateView({
selector: '.due-date-input',
parentView: this,
model: this.model
}),
new GradingView({
selector: '.edit-settings-grading',
parentView: this,
model: this.model
})
);
}
}
});
BaseDateView = Backbone.View.extend({
// Attribute name in the model, should be defined in children classes.
fieldName: null,
events : {
'click .clear-date': 'clearValue'
},
afterRender: function () {
this.setElement(this.options.parentView.$(this.options.selector).get(0));
this.$('input.date').datepicker({'dateFormat': 'm/d/yy'});
this.$('input.time').timepicker({
'timeFormat' : 'H:i',
'forceRoundTime': true
});
if (this.model.get(this.fieldName)) {
DateUtils.setDate(
this.$('input.date'), this.$('input.time'),
this.model.get(this.fieldName)
);
}
}
});
DueDateView = BaseDateView.extend({
fieldName: 'due',
getValue: function () {
return DateUtils.getDate(this.$('#due_date'), this.$('#due_time'));
},
clearValue: function (event) {
event.preventDefault();
this.$('#due_time, #due_date').val('');
},
getMetadata: function () {
return {
'due': this.getValue()
};
}
});
ReleaseDateView = BaseDateView.extend({
fieldName: 'start',
startingReleaseDate: null,
afterRender: function () {
BaseDateView.prototype.afterRender.call(this);
// Store the starting date and time so that we can determine if the user
// actually changed it when "Save" is pressed.
this.startingReleaseDate = this.getValue();
},
getValue: function () {
return DateUtils.getDate(this.$('#start_date'), this.$('#start_time'));
},
clearValue: function (event) {
event.preventDefault();
this.$('#start_time, #start_date').val('');
},
getMetadata: function () {
var newReleaseDate = this.getValue();
if (JSON.stringify(newReleaseDate) === JSON.stringify(this.startingReleaseDate)) {
return {};
}
return {
'start': newReleaseDate
};
}
});
GradingView = Backbone.View.extend({
afterRender: function () {
this.setElement(this.options.parentView.$(this.options.selector).get(0));
this.setValue(this.model.get('format'));
},
setValue: function (value) {
this.$('#grading_type').val(value);
},
getValue: function () {
return this.$('#grading_type').val();
},
getRequestData: function () {
return {
'graderType': this.getValue()
};
},
getContext: function () {
return {
graderTypes: JSON.parse(this.model.get('course_graders'))
};
}
});
StaffLockView = Backbone.View.extend({
isModelLocked: function() {
return this.model.get('has_explicit_staff_lock');
},
isAncestorLocked: function() {
return this.model.get('ancestor_has_staff_lock');
},
afterRender: function () {
this.setElement(this.options.parentView.$(this.options.selector).get(0));
this.setLock(this.isModelLocked());
},
setLock: function(value) {
this.$('#staff_lock').prop('checked', value);
},
isLocked: function() {
return this.$('#staff_lock').is(':checked');
},
hasChanges: function() {
return this.isModelLocked() != this.isLocked();
},
getRequestData: function() {
return this.hasChanges() ? { publish: 'republish' } : {};
},
getMetadata: function() {
// Setting visible_to_staff_only to null when disabled will delete the field from this
// xblock, allowing it to inherit the value of its ancestors.
return this.hasChanges() ? { visible_to_staff_only: this.isLocked() ? true : null } : {};
},
getContext: function () {
return {
hasExplicitStaffLock: this.isModelLocked(),
ancestorLocked: this.isAncestorLocked()
}
}
});
return EditSectionXBlockModal;
});
...@@ -234,8 +234,11 @@ ...@@ -234,8 +234,11 @@
} }
} }
// edit outline item settings // outline: edit item settings
.edit-outline-item-modal { .wrapper-modal-window-bulkpublish-section,
.wrapper-modal-window-bulkpublish-subsection,
.wrapper-modal-window-bulkpublish-unit,
.course-outline-modal {
.list-fields { .list-fields {
......
...@@ -263,7 +263,6 @@ ...@@ -263,7 +263,6 @@
// outline UI // outline UI
// -------------------- // --------------------
// outline: utilities // outline: utilities
$outline-indent-width: $baseline; $outline-indent-width: $baseline;
...@@ -294,82 +293,20 @@ $outline-indent-width: $baseline; ...@@ -294,82 +293,20 @@ $outline-indent-width: $baseline;
} }
// UI: section %outline-item-status {
%outline-section { @extend %t-copy-sub2;
@include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s); @extend %t-strong;
border-left: 1px solid $color-draft; color: $color-copy-base;
margin-bottom: $baseline;
padding: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
// STATE: is-collapsed
&.is-collapsed {
border-left-width: ($baseline/4);
padding-left: $baseline;
// CASE: is ready to be live
&.is-ready {
border-left-color: $color-ready;
}
// CASE: is live
&.is-live {
border-left-color: $color-live;
}
// CASE: has staff-only content
&.is-staff-only {
border-left-color: $color-staff-only;
}
// CASE: has unpublished content
&.has-warnings {
border-left-color: $color-warning;
}
// CASE: has errors
&.has-errors {
border-left-color: $color-error;
}
}
}
// UI: subsection
%outline-subsection {
@include transition(border-left-color $tmg-f2 linear 0s);
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/4) ($baseline/2) ($baseline/2) ($baseline/2);
// CASE: is ready to be live
&.is-ready {
border-left-color: $color-ready;
}
// CASE: is live
&.is-live {
border-left-color: $color-live;
}
// CASE: is presented for staff only
&.is-staff-only {
border-left-color: $color-staff-only;
}
// CASE: has unpublished content
&.has-warnings {
border-left-color: $color-warning;
}
// CASE: has errors .icon {
&.has-errors { @extend %t-icon5;
border-left-color: $color-error; margin-right: ($baseline/4);
} }
} }
%outline-item { // outline UI - complex
// --------------------
%outline-complex-item {
// UI: item title // UI: item title
.item-title { .item-title {
...@@ -424,154 +361,326 @@ $outline-indent-width: $baseline; ...@@ -424,154 +361,326 @@ $outline-indent-width: $baseline;
} }
} }
%outline-item-status {
@extend %t-copy-sub2;
@extend %t-strong;
color: $color-copy-base;
.icon { // outline UI - simple
@extend %t-icon5; // --------------------
margin-right: ($baseline/4); %outline-simple-item {
border: 1px solid $gray-l4;
// CASE: last-child in UI
&:last-child {
margin-bottom: 0;
} }
}
// outline: sections .item-title a {
.outline-section { color: $color-heading-base;
@extend %ui-window;
@extend %outline-item;
@extend %outline-section;
// header - title &:hover {
.section-title { color: $blue;
@extend %t-title5; }
@extend %t-strong;
color: $color-heading-base;
} }
}
// status
.section-status {
@extend %outline-item-status;
}
// status - release // CASE: complex outline
.status-release { .outline-complex {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
}
// status - grading // outline: sections
.status-grading { .outline-section {
@include transition(opacity $tmg-f2 ease-in-out 0s); @include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s);
opacity: 0.65; @extend %ui-window;
} @extend %outline-complex-item;
border-left: 1px solid $color-draft;
margin-bottom: $baseline;
padding: ($baseline/4) ($baseline/2) ($baseline/2) ($baseline/2);
.status-grading-value { // STATE: is-collapsed
display: inline-block; &.is-collapsed {
vertical-align: middle; border-left-width: ($baseline/4);
} padding-left: $baseline;
.status-grading-date { // CASE: is ready to be live
display: inline-block; &.is-ready {
vertical-align: middle; border-left-color: $color-ready;
margin-left: ($baseline/4); }
}
// CASE: is live
&.is-live {
border-left-color: $color-live;
}
// CASE: has staff-only content
&.is-staff-only {
border-left-color: $color-staff-only;
}
// status - message // CASE: has unpublished content
.status-message { &.has-warnings {
margin-top: ($baseline/2); border-left-color: $color-warning;
border-top: 1px solid $gray-l4; }
padding-top: ($baseline/4);
.icon { // CASE: has errors
margin-right: ($baseline/4); &.has-errors {
border-left-color: $color-error;
}
} }
}
.status-message-copy { // header - title
display: inline-block; .section-title {
color: $color-heading-base; @extend %t-title5;
} @extend %t-strong;
color: $color-heading-base;
}
// STATE: hover/active // status
&:hover, &:active { .section-status {
@extend %outline-item-status;
}
// status - release // status - release
> .section-status .status-release { .status-release {
opacity: 1.0; @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
} }
}
}
// outline: subsections // status - message
.outline-subsection { .status-message {
@extend %outline-item; margin-top: ($baseline/2);
@extend %outline-subsection; border-top: 1px solid $gray-l4;
border: 1px solid $gray-l4; padding-top: ($baseline/4);
border-left: ($baseline/4) solid $color-draft;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding: ($baseline*0.75);
// STATE: hover/active
&:hover, &:active {
box-shadow: 0 1px 1px $shadow-l2;
}
// STATE: is-collapsed .icon {
&.is-collapsed { margin-right: ($baseline/4);
}
}
.status-message-copy {
display: inline-block;
color: $color-heading-base;
}
// STATE: hover/active
&:hover, &:active {
// status - release
> .section-status .status-release {
opacity: 1.0;
}
}
} }
// header - title // outline: subsections
.subsection-title { .outline-subsection {
@extend %t-title6; @include transition(border-left-color $tmg-f2 linear 0s);
color: $color-heading-base; @extend %outline-complex-item;
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);
// CASE: is ready to be live
&.is-ready {
border-left-color: $color-ready;
}
// CASE: is live
&.is-live {
border-left-color: $color-live;
}
// CASE: is presented for staff only
&.is-staff-only {
border-left-color: $color-staff-only;
}
// CASE: has unpublished content
&.has-warnings {
border-left-color: $color-warning;
}
// CASE: has errors
&.has-errors {
border-left-color: $color-error;
}
// STATE: hover/active
&:hover, &:active {
box-shadow: 0 1px 1px $shadow-l2;
}
// STATE: is-collapsed
&.is-collapsed {
}
// header - title
.subsection-title {
@extend %t-title6;
color: $color-heading-base;
}
// status
.subsection-status {
@extend %outline-item-status;
}
// STATE: hover/active
&:hover, &:active {
// status - release
> .subsection-status .status-release {
opacity: 1.0;
}
// status - grading
> .subsection-status .status-grading {
opacity: 1.0;
}
}
// status - grading
.status-grading {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
}
.status-grading-value {
display: inline-block;
vertical-align: middle;
}
.status-grading-date {
display: inline-block;
vertical-align: middle;
margin-left: ($baseline/4);
}
} }
// status // outline: units
.subsection-status { .outline-unit {
@extend %outline-item-status; @extend %outline-complex-item;
margin-bottom: ($baseline/2);
border: 1px solid $gray-l4;
padding: ($baseline/4) ($baseline/2);
// header - title
.unit-title {
@extend %t-title7;
color: $color-heading-base;
}
.unit-status {
@extend %outline-item-status;
}
// STATE: hover/active
&:hover, &:active {
box-shadow: 0 1px 1px $shadow-l2;
// status - release
.unit-status .status-release {
opacity: 1.0;
}
}
} }
}
// STATE: hover/active // CASE: simple outline
&:hover, &:active { .outline-simple {
// outline: sections
.outline-section {
@extend %outline-simple-item;
margin-bottom: $baseline;
padding: ($baseline/2);
// header - title
.section-title {
@extend %t-title5;
@extend %t-strong;
color: $color-heading-base;
}
// status
.section-status {
@extend %outline-item-status;
}
// status - release // status - release
> .subsection-status .status-release { .status-release {
opacity: 1.0; @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
} }
// status - grading // status - grading
> .subsection-status .status-grading { .status-grading {
opacity: 1.0; @include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
} }
}
}
// outline: units .status-grading-value {
.outline-unit { display: inline-block;
@extend %outline-item; vertical-align: middle;
margin-bottom: ($baseline/2); }
border: 1px solid $gray-l4;
padding: ($baseline/4) ($baseline/2);
// header - title .status-grading-date {
.unit-title { display: inline-block;
@extend %t-title7; vertical-align: middle;
color: $color-heading-base; margin-left: ($baseline/4);
}
// status - message
.status-message {
margin-top: ($baseline/2);
border-top: 1px solid $gray-l4;
padding-top: ($baseline/4);
.icon {
margin-right: ($baseline/4);
}
}
.status-message-copy {
display: inline-block;
color: $color-heading-base;
}
} }
.unit-status { // outline: subsections
@extend %outline-item-status; .outline-subsection {
@extend %outline-simple-item;
margin-bottom: ($baseline/2);
padding: ($baseline/2);
// header - title
.subsection-title {
@extend %t-title6;
color: $color-heading-base;
}
// status
.subsection-status {
@extend %outline-item-status;
}
} }
// STATE: hover/active // outline: units
&:hover, &:active { .outline-unit {
box-shadow: 0 1px 1px $shadow-l2; @extend %outline-simple-item;
margin-bottom: ($baseline/4);
padding: ($baseline/4) ($baseline/2);
// status - release // header - title
.unit-status .status-release { .unit-title {
opacity: 1.0; @extend %t-title7;
color: $color-heading-base;
}
.unit-status {
@extend %outline-item-status;
} }
} }
} }
......
...@@ -259,34 +259,10 @@ ...@@ -259,34 +259,10 @@
} }
.wrapper-unit-tree-location { .wrapper-unit-tree-location {
// tree location-specific styles should go here
.outline-section{
box-shadow: none;
border: 0;
padding: 0;
}
.outline-subsection {
border-left: 1px solid $gray-l4;
padding: ($baseline/2);
.subsection-header {
margin-bottom: ($baseline/2);
}
}
.item-title { .item-title {
@extend %cont-text-wrap; @extend %cont-text-wrap;
} }
.item-title a {
color: $color-heading-base;
&:hover {
color: $blue;
}
}
} }
} }
} }
......
...@@ -220,291 +220,298 @@ ...@@ -220,291 +220,298 @@
// outline // outline
// -------------------- // --------------------
.outline {
.outline-content { // UI: simple version of the outline
margin-top: 0; .outline-simple {
}
// add/new items }
.add-item {
margin-top: ($baseline*0.75);
.button-new { // UI: complex version of the outline
@extend %ui-btn-flat-outline; .outline-complex {
padding: ($baseline/2) $baseline;
display: block;
.icon { .outline-content {
display: inline-block; margin-top: 0;
vertical-align: middle;
margin-right: ($baseline/2);
}
}
} }
.add-section { // outline: items
margin-bottom: $baseline; .outline-item {
}
.add-subsection { // CASE: expand/collapse-able
&.is-collapsible {
} // only select the current item's toggle expansion controls
&:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title {
.add-unit { // STATE: hover/active
margin-left: $outline-indent-width; &:hover, &:active {
} color: $blue;
} }
}
&.is-dragging {
@include transition-property(none);
}
}
// outline: items // item: title
.outline-item { .item-title {
// CASE: expand/collapse-able // STATE: is-editable
&.is-collapsible { &.is-editable {
// only select the current item's toggle expansion controls // editor
&:nth-child(1) .ui-toggle-expansion, &:nth-child(1) .item-title { + .editor {
display: block;
// STATE: hover/active .item-edit-title {
&:hover, &:active { width: 100%;
color: $blue; }
}
} }
} }
&.is-dragging { // STATE: drag and drop
@include transition-property(none); .drop-target-prepend .draggable-drop-indicator-initial {
opacity: 1.0;
} }
}
// item: title // STATE: was dropped
.item-title { &.was-dropped {
border-color: $blue;
}
}
// STATE: is-editable // outline: sections
&.is-editable { // --------------------
.outline-section {
padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4);
// editor // header
+ .editor { .section-header {
display: block; @extend %outline-item-header;
.item-edit-title { .incontext-editor-input {
width: 100%; @extend %t-strong;
} @extend %t-title5;
} }
} }
}
// STATE: drag and drop .section-header-details {
.drop-target-prepend .draggable-drop-indicator-initial { float: left;
opacity: 1.0; width: flex-grid(6, 9);
}
// STATE: was dropped .icon, .wrapper-section-title {
&.was-dropped { display: inline-block;
border-color: $blue; vertical-align: top;
} }
}
// outline: sections .icon {
// -------------------- margin-right: ($baseline/4);
.outline-section { }
padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4);
// header .wrapper-section-title {
.section-header { width: flex-grid(5, 6);
@extend %outline-item-header; line-height: 0;
}
}
.section-header-actions {
float: right;
width: flex-grid(3, 9);
margin-top: -($baseline/4);
text-align: right;
.incontext-editor-input { .actions-list {
@extend %t-strong; @extend %actions-list;
@extend %t-title5; @extend %t-action2;
}
} }
}
.section-header-details { // in-context actions
float: left; .incontext-editor-action-wrapper {
width: flex-grid(6, 9); top: -($baseline/20);
}
.icon, .wrapper-section-title { // status
display: inline-block; .section-status {
vertical-align: top; margin: 0 0 0 ($outline-indent-width*1.25);
} }
.icon { // content
margin-right: ($baseline/4); .section-content {
@extend %outline-item-content-shown;
} }
.wrapper-section-title { // CASE: is-collapsible
width: flex-grid(5, 6); &.is-collapsible {
line-height: 0; @extend %ui-expand-collapse;
.ui-toggle-expansion {
@extend %t-icon3;
color: $gray-l3;
}
} }
}
.section-header-actions { // STATE: is-collapsed
float: right; &.is-collapsed {
width: flex-grid(3, 9);
margin-top: -($baseline/4);
text-align: right;
.actions-list { .section-content {
@extend %actions-list; @extend %outline-item-content-hidden;
@extend %t-action2; }
} }
}
// in-context actions // STATE: drag and drop - was dropped
.incontext-editor-action-wrapper { &.was-dropped {
top: -($baseline/20); border-left-color: $ui-action-primary-color-focus;
}
} }
// status // outline: subsections
.section-status { // --------------------
margin: 0 0 0 ($outline-indent-width*1.25); .list-subsections {
margin: $baseline 0 0 0;
} }
// content .outline-subsection {
.section-content { padding: ($baseline*0.75);
@extend %outline-item-content-shown;
}
// CASE: is-collapsible // header
&.is-collapsible { .subsection-header {
@extend %ui-expand-collapse; @extend %outline-item-header;
.ui-toggle-expansion { .incontext-editor-input {
@extend %t-icon3; @extend %t-title6;
color: $gray-l3; }
} }
}
// STATE: is-collapsed .subsection-header-details {
&.is-collapsed { float: left;
width: flex-grid(6, 9);
.section-content { .icon, .wrapper-subsection-title {
@extend %outline-item-content-hidden; display: inline-block;
} vertical-align: top;
} }
// STATE: drag and drop - was dropped
&.was-dropped {
border-left-color: $ui-action-primary-color-focus;
}
}
// outline: subsections .icon {
// -------------------- margin-right: ($baseline/4);
.list-subsections { }
margin: $baseline 0 0 0;
}
.outline-subsection { .wrapper-subsection-title {
padding: ($baseline*0.75); width: flex-grid(5, 6);
margin-top: -($baseline/10);
line-height: 0;
}
}
// header .subsection-header-actions {
.subsection-header { float: right;
@extend %outline-item-header; width: flex-grid(3, 9);
margin-top: -($baseline/4);
text-align: right;
.incontext-editor-input { .actions-list {
@extend %t-title6; @extend %actions-list;
@extend %t-action2;
margin-right: ($baseline/2);
}
} }
}
.subsection-header-details {
float: left;
width: flex-grid(6, 9);
.icon, .wrapper-subsection-title { // in-context actions
display: inline-block; .incontext-editor-action-wrapper {
vertical-align: top; top: -($baseline/10);
} }
.icon { // status
margin-right: ($baseline/4); .subsection-status {
margin: 0 0 0 $outline-indent-width;
} }
.wrapper-subsection-title { // content
width: flex-grid(5, 6); .subsection-content {
margin-top: -($baseline/10); @extend %outline-item-content-shown;
line-height: 0;
} }
}
.subsection-header-actions { // CASE: is-collapsible
float: right; &.is-collapsible {
width: flex-grid(3, 9); @extend %ui-expand-collapse;
margin-top: -($baseline/4);
text-align: right;
.actions-list { .ui-toggle-expansion {
@extend %actions-list; @extend %t-icon4;
@extend %t-action2; color: $gray-l3;
margin-right: ($baseline/2); }
} }
}
// in-context actions // STATE: is-collapsed
.incontext-editor-action-wrapper { &.is-collapsed {
top: -($baseline/10);
}
// status .subsection-content {
.subsection-status { @extend %outline-item-content-hidden;
margin: 0 0 0 $outline-indent-width; }
}
} }
// content // outline: units
.subsection-content { // --------------------
@extend %outline-item-content-shown; .list-units {
margin: $baseline 0 0 0;
} }
// CASE: is-collapsible .outline-unit {
&.is-collapsible { @include transition(margin $tmg-f2 linear 0s); // needed for drag and drop transitions
@extend %ui-expand-collapse; margin-left: $outline-indent-width;
.ui-toggle-expansion { // header
@extend %t-icon4; .unit-header {
color: $gray-l3; @extend %outline-item-header;
} }
}
// STATE: is-collapsed .unit-header-details {
&.is-collapsed { float: left;
width: flex-grid(6, 9);
margin-top: ($baseline/4);
}
.subsection-content { .unit-header-actions {
@extend %outline-item-content-hidden; float: right;
width: flex-grid(3, 9);
margin-top: -($baseline/10);
text-align: right;
.actions-list {
@extend %actions-list;
@extend %t-action2;
}
} }
} }
}
// outline: units // add/new items
// -------------------- .add-item {
.list-units { margin-top: ($baseline*0.75);
margin: $baseline 0 0 0;
}
.outline-unit { .button-new {
@include transition(margin $tmg-f2 linear 0s); // needed for drag and drop transitions @extend %ui-btn-flat-outline;
margin-left: $outline-indent-width; padding: ($baseline/2) $baseline;
display: block;
// header .icon {
.unit-header { display: inline-block;
@extend %outline-item-header; vertical-align: middle;
margin-right: ($baseline/2);
}
}
} }
.unit-header-details { .add-section {
float: left; margin-bottom: $baseline;
width: flex-grid(6, 9);
margin-top: ($baseline/4);
} }
.unit-header-actions { .add-subsection {
float: right;
width: flex-grid(3, 9);
margin-top: -($baseline/10);
text-align: right;
.actions-list { }
@extend %actions-list;
@extend %t-action2; .add-unit {
} margin-left: $outline-indent-width;
} }
} }
...@@ -587,4 +594,147 @@ ...@@ -587,4 +594,147 @@
bottom: -($baseline/2); bottom: -($baseline/2);
} }
} }
// outline: edit item settings
.wrapper-modal-window-bulkpublish-section,
.wrapper-modal-window-bulkpublish-subsection,
.wrapper-modal-window-bulkpublish-unit,
.course-outline-modal {
.list-fields {
.field {
display: inline-block;
vertical-align: top;
margin-right: ($baseline/2);
margin-bottom: ($baseline/4);
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
label, input, textarea {
display: block;
}
label {
@extend %t-copy-sub1;
@include transition(color $tmg-f3 ease-in-out 0s);
margin: 0 0 ($baseline/4) 0;
font-weight: 600;
&.is-focused {
color: $blue;
}
}
input, textarea {
@extend %t-copy-base;
@include transition(all $tmg-f2 ease-in-out 0s);
height: 100%;
width: 100%;
padding: ($baseline/2);
// CASE: long length
&.long {
width: 100%;
}
// CASE: short length
&.short {
width: 25%;
}
}
// CASE: specific release + due times/dates
.start-date,
.start-time,
.due-date,
.due-time {
width: ($baseline*7);
}
}
// CASE: select input
.field-select {
.label, .input {
display: inline-block;
vertical-align: middle;
}
.label {
margin-right: ($baseline/2);
}
.input {
width: 100%;
}
}
}
.edit-settings-grading {
.grading-type {
margin-bottom: $baseline;
}
}
}
// outline: bulk publishing items
.bulkpublish-section-modal,
.bulkpublish-subsection-modal,
.bulkpublish-unit-modal {
.modal-introduction {
color: $gray-d2;
}
.modal-section .outline-bulkpublish {
max-height: ($baseline*20);
overflow-y: auto;
}
.outline-section,
.outline-subsection {
border: none;
padding: 0;
}
.outline-subsection {
margin-bottom: $baseline;
padding-right: ($baseline/4);
}
.outline-subsection .subsection-title {
@extend %t-title8;
margin-bottom: ($baseline/4);
font-weight: 600;
color: $gray-l2;
text-transform: uppercase;
}
.outline-unit .unit-title, .outline-unit .unit-status {
display: inline-block;
vertical-align: middle;
}
.outline-unit .unit-title {
@extend %t-title7;
color: $color-heading-base;
}
.outline-unit .unit-status {
@extend %t-copy-sub2;
text-align: right;
}
}
// it is the only element there
.bulkpublish-unit-modal {
.modal-introduction {
margin-bottom: 0;
}
}
} }
...@@ -144,7 +144,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal", ...@@ -144,7 +144,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
</div> </div>
<div class="wrapper-unit-tree-location bar-mod-content"> <div class="wrapper-unit-tree-location bar-mod-content">
<h5 class="title">${_("Location in Course Outline")}</h5> <h5 class="title">${_("Location in Course Outline")}</h5>
<div class="wrapper-unit-overview"> <div class="wrapper-unit-overview outline outline-simple">
</div> </div>
</div> </div>
</div> </div>
......
...@@ -29,7 +29,7 @@ from contentstore.utils import reverse_usage_url ...@@ -29,7 +29,7 @@ from contentstore.utils import reverse_usage_url
<%block name="header_extras"> <%block name="header_extras">
<link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" /> <link rel="stylesheet" type="text/css" href="${static.url('js/vendor/timepicker/jquery.timepicker.css')}" />
% for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'edit-outline-item-modal']: % for template_name in ['course-outline', 'xblock-string-field-editor', 'basic-modal', 'modal-button', 'course-outline-modal', 'due-date-editor', 'release-date-editor', 'grading-editor', 'publish-editor', 'staff-lock-editor']:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="js/${template_name}.underscore" /> <%static:include path="js/${template_name}.underscore" />
</script> </script>
...@@ -91,7 +91,7 @@ from contentstore.utils import reverse_usage_url ...@@ -91,7 +91,7 @@ from contentstore.utils import reverse_usage_url
course_locator = context_course.location course_locator = context_course.location
%> %>
<h2 class="sr">${_("Course Outline")}</h2> <h2 class="sr">${_("Course Outline")}</h2>
<article class="outline outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}"> <article class="outline outline-complex outline-course" data-locator="${course_locator}" data-course-key="${course_locator.course_key}">
</article> </article>
</div> </div>
<div class="ui-loading"> <div class="ui-loading">
...@@ -116,5 +116,4 @@ from contentstore.utils import reverse_usage_url ...@@ -116,5 +116,4 @@ from contentstore.utils import reverse_usage_url
</aside> </aside>
</section> </section>
</div> </div>
<footer></footer>
</%block> </%block>
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
<div class="message modal-introduction">
<p><%= introductionMessage %></p>
</div>
<div class="modal-section"></div>
</div>
<% <%
var category = xblockInfo.get('category');
var releasedToStudents = xblockInfo.get('released_to_students'); var releasedToStudents = xblockInfo.get('released_to_students');
var visibilityState = xblockInfo.get('visibility_state'); var visibilityState = xblockInfo.get('visibility_state');
var published = xblockInfo.get('published'); var published = xblockInfo.get('published');
...@@ -10,7 +9,7 @@ if (staffOnlyMessage) { ...@@ -10,7 +9,7 @@ if (staffOnlyMessage) {
statusType = 'staff-only'; statusType = 'staff-only';
statusMessage = gettext('Contains staff only content'); statusMessage = gettext('Contains staff only content');
} else if (visibilityState === 'needs_attention') { } else if (visibilityState === 'needs_attention') {
if (category === 'vertical') { if (xblockInfo.isVertical()) {
statusType = 'warning'; statusType = 'warning';
if (published && releasedToStudents) { if (published && releasedToStudents) {
statusMessage = gettext('Unpublished changes to live content'); statusMessage = gettext('Unpublished changes to live content');
...@@ -63,6 +62,14 @@ if (xblockInfo.get('graded')) { ...@@ -63,6 +62,14 @@ if (xblockInfo.get('graded')) {
<div class="<%= xblockType %>-header-actions"> <div class="<%= xblockType %>-header-actions">
<ul class="actions-list"> <ul class="actions-list">
<% if (xblockInfo.isPublishable()) { %>
<li class="action-item action-publish">
<a href="#" data-tooltip="<%= gettext('Publish') %>" class="publish-button action-button">
<i class="icon icon-upload-alt"></i>
<span class="sr action-button-text"><%= gettext('Publish') %></span>
</a>
</li>
<% } %>
<% if (xblockInfo.isEditableOnCourseOutline()) { %> <% if (xblockInfo.isEditableOnCourseOutline()) { %>
<li class="action-item action-configure"> <li class="action-item action-configure">
<a href="#" data-tooltip="<%= gettext('Configure') %>" class="configure-button action-button"> <a href="#" data-tooltip="<%= gettext('Configure') %>" class="configure-button action-button">
...@@ -141,7 +148,7 @@ if (xblockInfo.get('graded')) { ...@@ -141,7 +148,7 @@ if (xblockInfo.get('graded')) {
</a> </a>
</p> </p>
</div> </div>
<% } else if (category !== 'vertical') { %> <% } else if (!xblockInfo.isVertical()) { %>
<div class="outline-content <%= xblockType %>-content"> <div class="outline-content <%= xblockType %>-content">
<ol class="<%= typeListClass %> is-sortable"> <ol class="<%= typeListClass %> is-sortable">
<li class="ui-splint ui-splint-indicator"> <li class="ui-splint ui-splint-indicator">
......
<ul class="list-fields list-input datepair date-setter">
<li class="field field-text field-due-date">
<label for="due_date"><%= gettext('Due Date:') %></label>
<input type="text" id="due_date" name="due_date" value=""
placeholder="MM/DD/YYYY" class="due-date date input input-text" autocomplete="off"/>
</li>
<li class="field field-text field-due-time">
<label for="due_time"><%= gettext('Due Time in UTC:') %></label>
<input type="text" id="due_time" name="due_time" value=""
placeholder="HH:MM" class="due-time time input input-text" autocomplete="off" />
</li>
</ul>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%= gettext('Clear Grading Due Date') %>" class="clear-date action-button action-clear">
<i class="icon-undo"></i>
<span class="sr"><%= gettext('Clear Grading Due Date') %></span>
</a>
</li>
</ul>
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
<div class="message modal-introduction">
<p>
<%= interpolate(gettext("Change the settings for %(display_name)s"), {display_name: xblockInfo.get('display_name')}, true) %>
</p>
</div>
<form class="edit-settings-form" action="#">
<% if (xblockInfo.isChapter() || xblockInfo.isSequential()) { %>
<div class="modal-section edit-settings-release scheduled-date-input">
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
<div class="modal-section-content has-actions">
<ul class="list-fields list-input datepair">
<li class="field field-text field-start-date field-release-date">
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
<input type="text" id="start_date" name="start_date"
value=""
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
</li>
<li class="field field-text field-start-time field-release-time">
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
<input type="text" id="start_time" name="start_time"
value=""
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
</li>
</ul>
<% if (xblockInfo.isSequential()) { %>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%= gettext('Clear Release Date/Time') %>" class="clear-date action-button action-clear">
<i class="icon-undo"></i>
<span class="sr"><%= gettext('Clear Release Date/Time') %></span>
</a>
</li>
</ul>
<% } %>
</div>
</div>
<% } %>
<% if (xblockInfo.isSequential()) { %>
<div class="modal-section edit-settings-grading">
<h3 class="modal-section-title"><%= gettext('Grading') %></h3>
<div class="modal-section-content grading-type">
<ul class="list-fields list-input">
<li class="field field-grading-type field-select">
<label for="grading_type" class="label"><%= gettext('Grade as:') %></label>
<select class="input" id="grading_type">
<option value="notgraded"><%= gettext('Not Graded') %></option>
<% _.each(graderTypes, function(grader) { %>
<option value="<%= grader %>"><%= grader %></option>
<% }); %>
</select>
</li>
</ul>
</div>
<div class="modal-section-content has-actions due-date-input grading-due-date">
<ul class="list-fields list-input datepair date-setter">
<li class="field field-text field-due-date">
<label for="due_date"><%= gettext('Due Date:') %></label>
<input type="text" id="due_date" name="due_date"
value=""
placeholder="MM/DD/YYYY" class="due-date date input input-text" autocomplete="off"/>
</li>
<li class="field field-text field-due-time">
<label for="due_time"><%= gettext('Due Time in UTC:') %></label>
<input type="text" id="due_time" name="due_time"
value=""
placeholder="HH:MM" class="due-time time input input-text" autocomplete="off" />
</li>
</ul>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%= gettext('Clear Grading Due Date') %>" class="clear-date action-button action-clear">
<i class="icon-undo"></i>
<span class="sr"><%= gettext('Clear Grading Due Date') %></span>
</a>
</li>
</ul>
</div>
</div>
<% } %>
<div class="modal-section edit-staff-lock">
<h3 class="modal-section-title"><%= gettext('Student Visibility') %></h3>
<div class="modal-section-content staff-lock">
<ul class="list-fields list-input">
<li class="field field-checkbox checkbox-cosmetic">
<input type="checkbox" id="staff_lock" name="staff_lock" class="input input-checkbox" />
<label for="staff_lock" class="label">
<i class="icon-check input-checkbox-checked"></i>
<i class="icon-check-empty input-checkbox-unchecked"></i>
<%= gettext('Hide from students') %>
</label>
<% if (hasExplicitStaffLock && !ancestorLocked) { %>
<p class="tip tip-warning">
<% if (xblockInfo.isVertical()) { %>
<%= gettext('If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students.') %>
<% } else { %>
<% var message = gettext('If you make this %(xblockType)s visible to students, students will be able to see its content after the release date has passed and you have published the unit(s).'); %>
<%= interpolate(message, { xblockType: xblockType }, true) %>
<% } %>
</p>
<p class="tip tip-warning">
<% if (xblockInfo.isChapter()) { %>
<%= gettext('Any subsections or units that are explicitly hidden from students will remain hidden after you clear this option for the section.') %>
<% } %>
<% if (xblockInfo.isSequential()) { %>
<%= gettext('Any units that are explicitly hidden from students will remain hidden after you clear this option for the subsection.') %>
<% } %>
</p>
<% } %>
</li>
</ul>
</div>
</div>
</form>
</div>
<h3 class="modal-section-title"><%= gettext('Grading') %></h3>
<div class="modal-section-content grading-type">
<ul class="list-fields list-input">
<li class="field field-grading-type field-select">
<label for="grading_type" class="label"><%= gettext('Grade as:') %></label>
<select class="input" id="grading_type">
<option value="notgraded"><%= gettext('Not Graded') %></option>
<% _.each(graderTypes, function(grader) { %>
<option value="<%= grader %>"><%= grader %></option>
<% }); %>
</select>
</li>
</ul>
</div>
<% if (!xblockInfo.isVertical()) { %>
<div class="modal-section-content">
<div class="outline outline-simple outline-bulkpublish">
<% if (xblockInfo.isChapter()) { %>
<ol class="list-subsections">
<% _.each(xblockInfo.get('child_info').children, function(subsection) { %>
<% if (subsection.isPublishable()) { %>
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title"><%= subsection.get('display_name') %></h4>
<div class="subsection-content">
<ol class="list-units">
<% _.each(subsection.get('child_info').children, function(unit) { %>
<% if (unit.isPublishable()) { %>
<li class="outline-item outline-unit">
<span class="unit-title item-title"><%= unit.get('display_name') %></span>
</li>
<% } %>
<% }); %>
</ol>
</div>
</li>
<% } %>
<% }); %>
</ol>
<% } else { %>
<ol class="list-units">
<% _.each(xblockInfo.get('child_info').children, function(unit) { %>
<% if (unit.isPublishable()) { %>
<li class="outline-item outline-unit">
<span class="unit-title item-title"><%= unit.get('display_name') %></span>
</li>
<% } %>
<% }); %>
</ol>
<% } %>
</div>
</div>
<% } %>
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
<div class="modal-section-content has-actions">
<ul class="list-fields list-input datepair">
<li class="field field-text field-start-date field-release-date">
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
<input type="text" id="start_date" name="start_date"
value=""
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
</li>
<li class="field field-text field-start-time field-release-time">
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
<input type="text" id="start_time" name="start_time"
value=""
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
</li>
</ul>
<% if (xblockInfo.isSequential()) { %>
<ul class="list-actions">
<li class="action-item">
<a href="#" data-tooltip="<%= gettext('Clear Release Date/Time') %>" class="clear-date action-button action-clear">
<i class="icon-undo"></i>
<span class="sr"><%= gettext('Clear Release Date/Time') %></span>
</a>
</li>
</ul>
<% } %>
</div>
<form>
<h3 class="modal-section-title"><%= gettext('Student Visibility') %></h3>
<div class="modal-section-content staff-lock">
<ul class="list-fields list-input">
<li class="field field-checkbox checkbox-cosmetic">
<input type="checkbox" id="staff_lock" name="staff_lock" class="input input-checkbox" />
<label for="staff_lock" class="label">
<i class="icon-check input-checkbox-checked"></i>
<i class="icon-check-empty input-checkbox-unchecked"></i>
<%= gettext('Hide from students') %>
</label>
<% if (hasExplicitStaffLock && !ancestorLocked) { %>
<p class="tip tip-warning">
<% if (xblockInfo.isVertical()) { %>
<%= gettext('If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students.') %>
<% } else { %>
<% var message = gettext('If you make this %(xblockType)s visible to students, students will be able to see its content after the release date has passed and you have published the unit(s).'); %>
<%= interpolate(message, { xblockType: xblockType }, true) %>
<% } %>
</p>
<p class="tip tip-warning">
<% if (xblockInfo.isChapter()) { %>
<%= gettext('Any subsections or units that are explicitly hidden from students will remain hidden after you clear this option for the section.') %>
<% } %>
<% if (xblockInfo.isSequential()) { %>
<%= gettext('Any units that are explicitly hidden from students will remain hidden after you clear this option for the subsection.') %>
<% } %>
</p>
<% } %>
</li>
</ul>
</div>
</form>
\ No newline at end of file
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-section" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
<div class="modal-window-overlay"></div>
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
<div class="bulkpublish-section-modal">
<div class="modal-header">
<h2 class="title modal-window-title">${_("Publish [section name]")}</h2>
</div>
<div class="modal-content">
<div class="message modal-introduction">
<p>${_("Publish all unpublished changes for this section?")}</p>
</div>
<div class="modal-section bulkpublish-included">
<h3 class="modal-section-title">${_("The following will be published:")}</h3>
<div class="modal-section-content">
<div class="outline outline-simple outline-bulkpublish">
<ol class="list-subsections">
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title">Subsection Title</h4>
<div class="subsection-content">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</div>
</li>
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title">Subsection Title</h4>
<div class="subsection-content">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</div>
</li>
<li class="outline-item outline-subsection">
<h4 class="subsection-title item-title">Subsection Title</h4>
<div class="subsection-content">
<ol class="list-units">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</ol>
</div>
</li>
</ol>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-publish">Publish</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-subsection" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
<div class="modal-window-overlay"></div>
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
<div class="bulkpublish-subsection-modal">
<div class="modal-header">
<h2 class="title modal-window-title">${_("Publish [subsection name]")}</h2>
</div>
<div class="modal-content">
<div class="message modal-introduction">
<p>${_("Publish all unpublished changes for this subsection?")}</p>
</div>
<div class="modal-section bulkpublish-included">
<h3 class="modal-section-title">${_("The following will be published:")}</h3>
<div class="modal-section-content">
<div class="outline outline-simple outline-bulkpublish">
<ol class="list-units">
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title that is really really really long and may span more than just one visual line of text.</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
<li class="outline-item outline-unit">
<span class="unit-title item-title">Unit Title</span>
</li>
</ol>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-publish">Publish</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper wrapper-modal-window wrapper-modal-window-bulkpublish-unit" aria-describedby="modal-window-description" aria-labelledby="modal-window-title" aria-hidden="" role="dialog">
<div class="modal-window-overlay"></div>
<div style="top: 5%; left: 30%;" class="modal-window confirm modal-med modal-type-confirm">
<div class="bulkpublish-unit-modal">
<div class="modal-header">
<h2 class="title modal-window-title">${_("Publish [unit name]")}</h2>
</div>
<div class="modal-content">
<div class="message modal-introduction">
<p>${_("Publish all unpublished changes for this unit?")}</p>
</div>
</div>
<div class="modal-actions">
<h3 class="sr">Actions</h3>
<ul>
<li class="action-item">
<a href="#" class="button action-primary action-publish">Publish</a>
</li>
<li class="action-item">
<a href="#" class="button action-cancel">Cancel</a>
</li>
</ul>
</div>
</div>
</div>
</div>
...@@ -77,7 +77,7 @@ class CourseOutlineItem(object): ...@@ -77,7 +77,7 @@ class CourseOutlineItem(object):
@property @property
def has_staff_lock_warning(self): def has_staff_lock_warning(self):
""" Returns True iff the 'Contains staff only content' message is visible """ """ Returns True if the 'Contains staff only content' message is visible """
return self.status_message == 'Contains staff only content' if self.has_status_message else False return self.status_message == 'Contains staff only content' if self.has_status_message else False
@property @property
...@@ -149,6 +149,22 @@ class CourseOutlineItem(object): ...@@ -149,6 +149,22 @@ class CourseOutlineItem(object):
element = self.q(css=self._bounded_selector(".status-grading-value")) element = self.q(css=self._bounded_selector(".status-grading-value"))
return element.first.text[0] if element.present else None return element.first.text[0] if element.present else None
def publish(self):
"""
Publish the unit.
"""
click_css(self, self._bounded_selector('.action-publish'), require_notification=False)
modal = CourseOutlineModal(self)
EmptyPromise(lambda: modal.is_shown(), 'Modal is shown.')
modal.publish()
@property
def publish_action(self):
"""
Returns the link for publishing a unit.
"""
return self.q(css=self._bounded_selector('.action-publish')).first
class CourseOutlineContainer(CourseOutlineItem): class CourseOutlineContainer(CourseOutlineItem):
""" """
...@@ -483,7 +499,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer): ...@@ -483,7 +499,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
class CourseOutlineModal(object): class CourseOutlineModal(object):
MODAL_SELECTOR = ".edit-outline-item-modal" MODAL_SELECTOR = ".wrapper-modal-window"
def __init__(self, page): def __init__(self, page):
self.page = page self.page = page
...@@ -507,6 +523,10 @@ class CourseOutlineModal(object): ...@@ -507,6 +523,10 @@ class CourseOutlineModal(object):
self.click(".action-save") self.click(".action-save")
self.page.wait_for_ajax() self.page.wait_for_ajax()
def publish(self):
self.click(".action-publish")
self.page.wait_for_ajax()
def cancel(self): def cancel(self):
self.click(".action-cancel") self.click(".action-cancel")
......
...@@ -11,6 +11,7 @@ from bok_choy.promise import EmptyPromise ...@@ -11,6 +11,7 @@ from bok_choy.promise import EmptyPromise
from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState
from ..pages.studio.utils import add_discussion from ..pages.studio.utils import add_discussion
from ..pages.lms.courseware import CoursewarePage from ..pages.lms.courseware import CoursewarePage
from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.staff_view import StaffPage from ..pages.lms.staff_view import StaffPage
from ..fixtures.course import XBlockFixtureDesc from ..fixtures.course import XBlockFixtureDesc
...@@ -1369,3 +1370,129 @@ class UnitNavigationTest(CourseOutlineTest): ...@@ -1369,3 +1370,129 @@ class UnitNavigationTest(CourseOutlineTest):
self.course_outline_page.section_at(0).subsection_at(0).toggle_expand() self.course_outline_page.section_at(0).subsection_at(0).toggle_expand()
unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to() unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to()
self.assertTrue(unit.is_browser_on_page) self.assertTrue(unit.is_browser_on_page)
@attr('shard_1')
class PublishSectionTest(CourseOutlineTest):
"""
Feature: Publish sections.
"""
__test__ = True
def populate_course_fixture(self, course_fixture):
"""
Sets up a course structure with 2 subsections inside a single section.
The first subsection has 2 units, and the second subsection has one unit.
"""
self.courseware = CoursewarePage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
course_fixture.add_children(
XBlockFixtureDesc('chapter', SECTION_NAME).add_children(
XBlockFixtureDesc('sequential', SUBSECTION_NAME).add_children(
XBlockFixtureDesc('vertical', UNIT_NAME),
XBlockFixtureDesc('vertical', 'Test Unit 2'),
),
XBlockFixtureDesc('sequential', 'Test Subsection 2').add_children(
XBlockFixtureDesc('vertical', 'Test Unit 3'),
),
),
)
def test_unit_publishing(self):
"""
Scenario: Can publish a unit and see published content in LMS
Given I have a section with 2 subsections and 3 unpublished units
When I go to the course outline
Then I see publish button for the first unit, subsection, section
When I publish the first unit
Then I see that publish button for the first unit disappears
And I see publish buttons for subsection, section
And I see the changed content in LMS
"""
self._add_unpublished_content()
self.course_outline_page.visit()
section, subsection, unit = self._get_items()
self.assertTrue(unit.publish_action)
self.assertTrue(subsection.publish_action)
self.assertTrue(section.publish_action)
unit.publish()
self.assertFalse(unit.publish_action)
self.assertTrue(subsection.publish_action)
self.assertTrue(section.publish_action)
self.courseware.visit()
self.assertEqual(1, self.courseware.num_xblock_components)
def test_subsection_publishing(self):
"""
Scenario: Can publish a subsection and see published content in LMS
Given I have a section with 2 subsections and 3 unpublished units
When I go to the course outline
Then I see publish button for the unit, subsection, section
When I publish the first subsection
Then I see that publish button for the first subsection disappears
And I see that publish buttons disappear for the child units of the subsection
And I see publish button for section
And I see the changed content in LMS
"""
self._add_unpublished_content()
self.course_outline_page.visit()
section, subsection, unit = self._get_items()
self.assertTrue(unit.publish_action)
self.assertTrue(subsection.publish_action)
self.assertTrue(section.publish_action)
self.course_outline_page.section(SECTION_NAME).subsection(SUBSECTION_NAME).publish()
self.assertFalse(unit.publish_action)
self.assertFalse(subsection.publish_action)
self.assertTrue(section.publish_action)
self.courseware.visit()
self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_sequential_position(2)
self.assertEqual(1, self.courseware.num_xblock_components)
def test_section_publishing(self):
"""
Scenario: Can publish a section and see published content in LMS
Given I have a section with 2 subsections and 3 unpublished units
When I go to the course outline
Then I see publish button for the unit, subsection, section
When I publish the section
Then I see that publish buttons disappears
And I see the changed content in LMS
"""
self._add_unpublished_content()
self.course_outline_page.visit()
section, subsection, unit = self._get_items()
self.assertTrue(subsection.publish_action)
self.assertTrue(section.publish_action)
self.assertTrue(unit.publish_action)
self.course_outline_page.section(SECTION_NAME).publish()
self.assertFalse(subsection.publish_action)
self.assertFalse(section.publish_action)
self.assertFalse(unit.publish_action)
self.courseware.visit()
self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_sequential_position(2)
self.assertEqual(1, self.courseware.num_xblock_components)
self.course_nav.go_to_section(SECTION_NAME, 'Test Subsection 2')
self.assertEqual(1, self.courseware.num_xblock_components)
def _add_unpublished_content(self):
"""
Adds unpublished HTML content to first three units in the course.
"""
for index in xrange(3):
self.course_fixture.create_xblock(
self.course_fixture.get_nested_xblocks(category="vertical")[index].locator,
XBlockFixtureDesc('html', 'Unpublished HTML Component ' + str(index)),
)
def _get_items(self):
"""
Returns first section, subsection, and unit on the page.
"""
section = self.course_outline_page.section(SECTION_NAME)
subsection = section.subsection(SUBSECTION_NAME)
unit = subsection.toggle_expand().unit(UNIT_NAME)
return (section, subsection, unit)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment