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):
resp = self._show_course_overview(course.id)
self.assertContains(
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',
course_key='MITx/999/Robot_Super_Course',
),
......
......@@ -181,6 +181,7 @@ def xblock_handler(request, usage_key_string):
content_type="text/plain"
)
# pylint: disable=unused-argument
@require_http_methods(("GET"))
@login_required
......@@ -635,7 +636,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
return None
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:
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
# 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
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
xblock_info = {
......@@ -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,
"published": published,
"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,
"release_date": release_date,
"visibility_state": visibility_state,
......@@ -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),
"format": xblock.format,
"course_graders": json.dumps([grader.get('type') for grader in graders]),
"has_changes": has_changes,
}
if data is not None:
xblock_info["data"] = data
......@@ -689,14 +694,13 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
else:
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
# that are not being rendered on the course outline.
if is_xblock_unit and not course_outline:
xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_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['has_changes'] = is_unit_with_changes
if release_date:
xblock_info["release_date_from"] = _get_release_date_from(xblock)
if visibility_state == VisibilityState.staff_only:
......
......@@ -59,7 +59,7 @@ function(Backbone, _, str, ModuleUtils) {
*/
"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,
/**
......@@ -153,6 +153,10 @@ function(Backbone, _, str, ModuleUtils) {
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 {Array}
......
......@@ -5,14 +5,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
describe("CourseOutlinePage", function() {
var createCourseOutlinePage, displayNameInput, model, outlinePage, requests,
getItemsOfType, getItemHeaders, verifyItemsExpanded, expandItemsAndVerifyState, collapseItemsAndVerifyState,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON,
createMockCourseJSON, createMockSectionJSON, createMockSubsectionJSON, verifyTypePublishable,
mockCourseJSON, mockEmptyCourseJSON, mockSingleSectionCourseJSON, createMockVerticalJSON,
mockOutlinePage = readFixtures('mock/mock-course-outline-page.underscore');
createMockCourseJSON = function(id, displayName, children) {
return {
id: id,
display_name: displayName,
createMockCourseJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-course',
display_name: 'Mock Course',
category: 'course',
studio_url: '/course/slashes:MockCourse',
is_container: true,
......@@ -22,17 +22,18 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
display_name: 'Section',
category: 'chapter',
children: children
display_name: 'Section',
children: []
}
}, options, {child_info: {children: children}});
};
};
createMockSectionJSON = function(id, displayName, children) {
return {
id: id,
createMockSectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-section',
display_name: 'Mock Section',
category: 'chapter',
display_name: displayName,
studio_url: '/course/slashes:MockCourse',
is_container: true,
has_changes: false,
......@@ -43,14 +44,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
child_info: {
category: 'sequential',
display_name: 'Subsection',
children: children
children: []
}
}, options, {child_info: {children: children}});
};
};
createMockSubsectionJSON = function(id, displayName, children) {
return {
id: id,
display_name: displayName,
createMockSubsectionJSON = function(options, children) {
return $.extend(true, {}, {
id: 'mock-subsection',
display_name: 'Mock Subsection',
category: 'sequential',
studio_url: '/course/slashes:MockCourse',
is_container: true,
......@@ -63,9 +65,24 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
child_info: {
category: 'vertical',
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) {
......@@ -108,40 +125,100 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
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 () {
view_helpers.installMockAnalytics();
view_helpers.installViewTemplates();
view_helpers.installTemplate('course-outline');
view_helpers.installTemplate('xblock-string-field-editor');
view_helpers.installTemplate('modal-button');
view_helpers.installTemplate('basic-modal');
view_helpers.installTemplate('edit-outline-item-modal');
view_helpers.installTemplates([
'course-outline', 'xblock-string-field-editor', 'modal-button',
'basic-modal', 'course-outline-modal', 'release-date-editor',
'due-date-editor', 'grading-editor', 'publish-editor',
'staff-lock-editor'
]);
appendSetFixtures(mockOutlinePage);
mockCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
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'
}])
mockCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON()
])
])
]);
mockEmptyCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', []);
mockSingleSectionCourseJSON = createMockCourseJSON('mock-course', 'Mock Course', [
createMockSectionJSON('mock-section', 'Mock Section', [])
mockEmptyCourseJSON = createMockCourseJSON();
mockSingleSectionCourseJSON = createMockCourseJSON({}, [
createMockSectionJSON()
]);
});
afterEach(function () {
view_helpers.removeMockAnalytics();
edit_helpers.cancelModalIfShowing();
// Clean up after the $.datepicker
$("#start_date").datepicker( "destroy" );
$("#due_date").datepicker( "destroy" );
$('.ui-datepicker').remove();
});
describe('Initial display', function() {
......@@ -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
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section-2');
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section-2', 'Mock Section 2', []));
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'}));
sectionElements = getItemsOfType('section');
expect(sectionElements.length).toBe(2);
expect($(sectionElements[0]).data('locator')).toEqual('mock-section');
......@@ -269,9 +346,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy(), requestCount;
createCourseOutlinePage(this, createMockCourseJSON('mock-course', 'Mock Course', [
createMockSectionJSON('mock-section', 'Mock Section', []),
createMockSectionJSON('mock-section-2', 'Mock Section 2', [])
createCourseOutlinePage(this, createMockCourseJSON({}, [
createMockSectionJSON(),
createMockSectionJSON({id: 'mock-section-2', display_name: 'Mock Section 2'})
]));
getItemHeaders('section').find('.delete-button').first().click();
view_helpers.confirmPrompt(promptSpy);
......@@ -356,7 +433,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
outlinePage.$('.section-header-actions .configure-button').click();
$("#start_date").val("1/2/2015");
// 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
expect($("due_date")).not.toExist();
......@@ -364,9 +441,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// Staff lock controls are always visible
expect($("#staff_lock")).toExist();
$(".edit-outline-item-modal .action-save").click();
$(".wrapper-modal-window .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
"metadata":{
"start":"2015-01-02T00:00:00.000Z"
......@@ -376,25 +451,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
// This is the response for the change operation.
create_sinon.respondWithJson(requests, {});
var mockResponseSectionJSON = $.extend(true, {},
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
var mockResponseSectionJSON = createMockSectionJSON({
release_date: 'Jan 02, 2015 at 00:00 UTC'
}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON({
has_changes: true,
published: false,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}
published: false
})
])
]),
{
release_date: 'Jan 02, 2015 at 00:00 UTC',
}
);
]);
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section')
expect(requests.length).toBe(2);
// 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"
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() {
......@@ -419,25 +525,10 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
};
// Contains hard-coded dates because dates are presented in different formats.
mockServerValuesJson = $.extend(true, {},
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
display_name: 'Mock Unit',
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
has_changes: true,
published: false,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}
])
]),
{
release_date: 'Jan 01, 2970 at 05:00 UTC',
child_info: { //Section child_info
children: [{ // Section children
var mockServerValuesJson = createMockSectionJSON({
release_date: 'Jan 01, 2970 at 05:00 UTC'
}, [
createMockSubsectionJSON({
graded: true,
due_date: 'Jul 10, 2014 at 00:00 UTC',
release_date: 'Jul 09, 2014 at 00:00 UTC',
......@@ -446,10 +537,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
due: "2014-07-10T00:00:00Z",
has_explicit_staff_lock: true,
staff_only_message: true
}]
}
}
);
}, [
createMockVerticalJSON({
has_changes: true,
published: false
})
])
]);
it('can be deleted', function() {
var promptSpy = view_helpers.createPromptSpy();
......@@ -492,9 +586,12 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
create_sinon.respondWithJson(requests, { });
// This is the response for the subsequent fetch operation for the section.
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', updatedDisplayName, [])
]));
createMockSectionJSON({}, [
createMockSubsectionJSON({
display_name: updatedDisplayName
})
])
);
// Find the display name again in the refreshed DOM and verify it
displayNameWrapper = getItemHeaders('subsection').find('.wrapper-xblock-field');
view_helpers.verifyInlineEditChange(displayNameWrapper, updatedDisplayName);
......@@ -514,7 +611,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
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', {
"graderType":"Lab",
"publish": "republish",
......@@ -550,7 +647,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
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.
create_sinon.respondWithJson(requests, {});
......@@ -568,29 +665,62 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($("#grading_type").val()).toBe('Lab');
expect($("#staff_lock").is(":checked")).toBe(true);
$(".edit-outline-item-modal .scheduled-date-input .action-clear").click();
$(".edit-outline-item-modal .due-date-input .action-clear").click();
$(".wrapper-modal-window .scheduled-date-input .action-clear").click();
$(".wrapper-modal-window .due-date-input .action-clear").click();
expect($("#start_date").val()).toBe('');
expect($("#due_date").val()).toBe('');
$("#grading_type").val('notgraded');
$("#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.
create_sinon.respondWithJson(requests, {});
// This is the response for the subsequent fetch operation.
create_sinon.respondWithJson(requests,
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [])
])
createMockSectionJSON({}, [createMockSubsectionJSON()])
);
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-value")).not.toExist();
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
......@@ -615,6 +745,16 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
unitAnchor = getItemsOfType('unit').find('.unit-title a');
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
});
verifyTypePublishable('unit', function (options) {
return createMockCourseJSON({}, [
createMockSectionJSON({}, [
createMockSubsectionJSON({}, [
createMockVerticalJSON(options)
])
])
]);
});
});
describe("Date and Time picker", function() {
......
......@@ -8,9 +8,12 @@
* - 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
*/
define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_utils",
"js/models/xblock_outline_info", "js/views/modals/edit_outline_item", "js/utils/drag_and_drop"],
function($, _, XBlockOutlineView, ViewUtils, XBlockOutlineInfo, EditSectionXBlockModal, ContentDragger) {
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/course_outline_modals", "js/utils/drag_and_drop"],
function(
$, _, XBlockOutlineView, ViewUtils, XBlockViewUtils,
XBlockOutlineInfo, CourseOutlineModalsFactory, ContentDragger
) {
var CourseOutlineView = XBlockOutlineView.extend({
// takes XBlockOutlineInfo as a model
......@@ -144,13 +147,30 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
},
editXBlock: function() {
var modal = new EditSectionXBlockModal({
model: this.model,
var modal = CourseOutlineModalsFactory.getModal('edit', this.model, {
onSave: this.refresh.bind(this),
parentInfo: this.parentInfo
parentInfo: this.parentInfo,
xblockType: XBlockViewUtils.getXBlockType(
this.model.get('category'), this.parentView.model, true
)
});
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) {
......@@ -159,6 +179,10 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
event.preventDefault();
this.editXBlock();
}.bind(this));
element.find('.publish-button').click(function(event) {
event.preventDefault();
this.publishXBlock();
}.bind(this));
},
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 @@
}
}
// edit outline item settings
.edit-outline-item-modal {
// outline: edit item settings
.wrapper-modal-window-bulkpublish-section,
.wrapper-modal-window-bulkpublish-subsection,
.wrapper-modal-window-bulkpublish-unit,
.course-outline-modal {
.list-fields {
......
......@@ -263,7 +263,6 @@
// outline UI
// --------------------
// outline: utilities
$outline-indent-width: $baseline;
......@@ -294,82 +293,20 @@ $outline-indent-width: $baseline;
}
// UI: section
%outline-section {
@include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s);
border-left: 1px solid $color-draft;
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;
}
%outline-item-status {
@extend %t-copy-sub2;
@extend %t-strong;
color: $color-copy-base;
// CASE: has errors
&.has-errors {
border-left-color: $color-error;
.icon {
@extend %t-icon5;
margin-right: ($baseline/4);
}
}
%outline-item {
// outline UI - complex
// --------------------
%outline-complex-item {
// UI: item title
.item-title {
......@@ -424,22 +361,69 @@ $outline-indent-width: $baseline;
}
}
%outline-item-status {
@extend %t-copy-sub2;
@extend %t-strong;
color: $color-copy-base;
.icon {
@extend %t-icon5;
margin-right: ($baseline/4);
// outline UI - simple
// --------------------
%outline-simple-item {
border: 1px solid $gray-l4;
// CASE: last-child in UI
&:last-child {
margin-bottom: 0;
}
.item-title a {
color: $color-heading-base;
&:hover {
color: $blue;
}
}
}
// outline: sections
.outline-section {
// CASE: complex outline
.outline-complex {
// outline: sections
.outline-section {
@include transition(border-left-width $tmg-f2 linear 0s, border-left-color $tmg-f2 linear 0s, padding-left $tmg-f2 linear 0s);
@extend %ui-window;
@extend %outline-item;
@extend %outline-section;
@extend %outline-complex-item;
border-left: 1px solid $color-draft;
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;
}
}
// header - title
.section-title {
......@@ -459,23 +443,6 @@ $outline-indent-width: $baseline;
opacity: 0.65;
}
// 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 - message
.status-message {
margin-top: ($baseline/2);
......@@ -500,18 +467,45 @@ $outline-indent-width: $baseline;
opacity: 1.0;
}
}
}
}
// outline: subsections
.outline-subsection {
@extend %outline-item;
@extend %outline-subsection;
// outline: subsections
.outline-subsection {
@include transition(border-left-color $tmg-f2 linear 0s);
@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;
......@@ -546,11 +540,28 @@ $outline-indent-width: $baseline;
opacity: 1.0;
}
}
}
// outline: units
.outline-unit {
@extend %outline-item;
// 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);
}
}
// outline: units
.outline-unit {
@extend %outline-complex-item;
margin-bottom: ($baseline/2);
border: 1px solid $gray-l4;
padding: ($baseline/4) ($baseline/2);
......@@ -574,5 +585,103 @@ $outline-indent-width: $baseline;
opacity: 1.0;
}
}
}
}
// CASE: simple outline
.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 {
@include transition(opacity $tmg-f2 ease-in-out 0s);
opacity: 0.65;
}
// 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 - 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;
}
}
// outline: subsections
.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;
}
}
// outline: units
.outline-unit {
@extend %outline-simple-item;
margin-bottom: ($baseline/4);
padding: ($baseline/4) ($baseline/2);
// header - title
.unit-title {
@extend %t-title7;
color: $color-heading-base;
}
.unit-status {
@extend %outline-item-status;
}
}
}
......@@ -259,34 +259,10 @@
}
.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 {
@extend %cont-text-wrap;
}
.item-title a {
color: $color-heading-base;
&:hover {
color: $blue;
}
}
}
}
}
......
......@@ -220,40 +220,17 @@
// outline
// --------------------
.outline {
.outline-content {
margin-top: 0;
}
// add/new items
.add-item {
margin-top: ($baseline*0.75);
.button-new {
@extend %ui-btn-flat-outline;
padding: ($baseline/2) $baseline;
display: block;
// UI: simple version of the outline
.outline-simple {
.icon {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/2);
}
}
}
.add-section {
margin-bottom: $baseline;
}
.add-subsection {
// UI: complex version of the outline
.outline-complex {
}
.add-unit {
margin-left: $outline-indent-width;
}
.outline-content {
margin-top: 0;
}
// outline: items
......@@ -508,6 +485,36 @@
}
}
// add/new items
.add-item {
margin-top: ($baseline*0.75);
.button-new {
@extend %ui-btn-flat-outline;
padding: ($baseline/2) $baseline;
display: block;
.icon {
display: inline-block;
vertical-align: middle;
margin-right: ($baseline/2);
}
}
}
.add-section {
margin-bottom: $baseline;
}
.add-subsection {
}
.add-unit {
margin-left: $outline-indent-width;
}
}
// UI: drag and drop: section
// --------------------
......@@ -587,4 +594,147 @@
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",
</div>
<div class="wrapper-unit-tree-location bar-mod-content">
<h5 class="title">${_("Location in Course Outline")}</h5>
<div class="wrapper-unit-overview">
<div class="wrapper-unit-overview outline outline-simple">
</div>
</div>
</div>
......
......@@ -29,7 +29,7 @@ from contentstore.utils import reverse_usage_url
<%block name="header_extras">
<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">
<%static:include path="js/${template_name}.underscore" />
</script>
......@@ -91,7 +91,7 @@ from contentstore.utils import reverse_usage_url
course_locator = context_course.location
%>
<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>
</div>
<div class="ui-loading">
......@@ -116,5 +116,4 @@ from contentstore.utils import reverse_usage_url
</aside>
</section>
</div>
<footer></footer>
</%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 visibilityState = xblockInfo.get('visibility_state');
var published = xblockInfo.get('published');
......@@ -10,7 +9,7 @@ if (staffOnlyMessage) {
statusType = 'staff-only';
statusMessage = gettext('Contains staff only content');
} else if (visibilityState === 'needs_attention') {
if (category === 'vertical') {
if (xblockInfo.isVertical()) {
statusType = 'warning';
if (published && releasedToStudents) {
statusMessage = gettext('Unpublished changes to live content');
......@@ -63,6 +62,14 @@ if (xblockInfo.get('graded')) {
<div class="<%= xblockType %>-header-actions">
<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()) { %>
<li class="action-item action-configure">
<a href="#" data-tooltip="<%= gettext('Configure') %>" class="configure-button action-button">
......@@ -141,7 +148,7 @@ if (xblockInfo.get('graded')) {
</a>
</p>
</div>
<% } else if (category !== 'vertical') { %>
<% } else if (!xblockInfo.isVertical()) { %>
<div class="outline-content <%= xblockType %>-content">
<ol class="<%= typeListClass %> is-sortable">
<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):
@property
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
@property
......@@ -149,6 +149,22 @@ class CourseOutlineItem(object):
element = self.q(css=self._bounded_selector(".status-grading-value"))
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):
"""
......@@ -483,7 +499,7 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
class CourseOutlineModal(object):
MODAL_SELECTOR = ".edit-outline-item-modal"
MODAL_SELECTOR = ".wrapper-modal-window"
def __init__(self, page):
self.page = page
......@@ -507,6 +523,10 @@ class CourseOutlineModal(object):
self.click(".action-save")
self.page.wait_for_ajax()
def publish(self):
self.click(".action-publish")
self.page.wait_for_ajax()
def cancel(self):
self.click(".action-cancel")
......
......@@ -11,6 +11,7 @@ from bok_choy.promise import EmptyPromise
from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState
from ..pages.studio.utils import add_discussion
from ..pages.lms.courseware import CoursewarePage
from ..pages.lms.course_nav import CourseNavPage
from ..pages.lms.staff_view import StaffPage
from ..fixtures.course import XBlockFixtureDesc
......@@ -1369,3 +1370,129 @@ class UnitNavigationTest(CourseOutlineTest):
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()
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