STUD-1911, STUD-1932: Publish sections, subsections and units from course outline.
This commit is contained in:
@@ -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
|
||||
@@ -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)
|
||||
# we should remove this once we can break this reference from the course to static tabs
|
||||
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.tabs.append(
|
||||
StaticTab(
|
||||
@@ -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,
|
||||
has_changes: true,
|
||||
published: false,
|
||||
edited_on: 'Jul 02, 2014 at 20:56 UTC',
|
||||
edited_by: 'MockUser'
|
||||
}
|
||||
var mockResponseSectionJSON = createMockSectionJSON({
|
||||
release_date: 'Jan 02, 2015 at 00:00 UTC'
|
||||
}, [
|
||||
createMockSubsectionJSON({}, [
|
||||
createMockVerticalJSON({
|
||||
has_changes: true,
|
||||
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,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.
|
||||
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'
|
||||
}
|
||||
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',
|
||||
start: "2014-07-09T00:00:00Z",
|
||||
format: "Lab",
|
||||
due: "2014-07-10T00:00:00Z",
|
||||
has_explicit_staff_lock: true,
|
||||
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() {
|
||||
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
|
||||
)
|
||||
});
|
||||
|
||||
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) {
|
||||
@@ -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) {
|
||||
|
||||
379
cms/static/js/views/modals/course_outline_modals.js
Normal file
379
cms/static/js/views/modals/course_outline_modals.js
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -1,299 +0,0 @@
|
||||
/**
|
||||
* 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);
|
||||
%outline-item-status {
|
||||
@extend %t-copy-sub2;
|
||||
@extend %t-strong;
|
||||
color: $color-copy-base;
|
||||
|
||||
// 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;
|
||||
}
|
||||
.icon {
|
||||
@extend %t-icon5;
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
&.has-errors {
|
||||
border-left-color: $color-error;
|
||||
}
|
||||
}
|
||||
|
||||
%outline-item {
|
||||
// outline UI - complex
|
||||
// --------------------
|
||||
%outline-complex-item {
|
||||
|
||||
// UI: item title
|
||||
.item-title {
|
||||
@@ -424,154 +361,326 @@ $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: sections
|
||||
.outline-section {
|
||||
@extend %ui-window;
|
||||
@extend %outline-item;
|
||||
@extend %outline-section;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
|
||||
// status - release
|
||||
> .section-status .status-release {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
.outline-subsection {
|
||||
@extend %outline-item;
|
||||
@extend %outline-subsection;
|
||||
// outline UI - simple
|
||||
// --------------------
|
||||
%outline-simple-item {
|
||||
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);
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
box-shadow: 0 1px 1px $shadow-l2;
|
||||
// CASE: last-child in UI
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
.item-title a {
|
||||
color: $color-heading-base;
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// header - title
|
||||
.subsection-title {
|
||||
@extend %t-title6;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
// CASE: complex outline
|
||||
.outline-complex {
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
// 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-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 {
|
||||
@extend %t-title5;
|
||||
@extend %t-strong;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// status - release
|
||||
> .subsection-status .status-release {
|
||||
opacity: 1.0;
|
||||
.status-release {
|
||||
@include transition(opacity $tmg-f2 ease-in-out 0s);
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
|
||||
// status - release
|
||||
> .section-status .status-release {
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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
|
||||
> .subsection-status .status-grading {
|
||||
opacity: 1.0;
|
||||
.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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
.outline-unit {
|
||||
@extend %outline-item;
|
||||
margin-bottom: ($baseline/2);
|
||||
border: 1px solid $gray-l4;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
// CASE: simple outline
|
||||
.outline-simple {
|
||||
|
||||
// header - title
|
||||
.unit-title {
|
||||
@extend %t-title7;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
// outline: sections
|
||||
.outline-section {
|
||||
@extend %outline-simple-item;
|
||||
margin-bottom: $baseline;
|
||||
padding: ($baseline/2);
|
||||
|
||||
.unit-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
// header - title
|
||||
.section-title {
|
||||
@extend %t-title5;
|
||||
@extend %t-strong;
|
||||
color: $color-heading-base;
|
||||
}
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
box-shadow: 0 1px 1px $shadow-l2;
|
||||
// status
|
||||
.section-status {
|
||||
@extend %outline-item-status;
|
||||
}
|
||||
|
||||
// status - release
|
||||
.unit-status .status-release {
|
||||
opacity: 1.0;
|
||||
.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,12 +220,271 @@
|
||||
|
||||
// outline
|
||||
// --------------------
|
||||
.outline {
|
||||
|
||||
// UI: simple version of the outline
|
||||
.outline-simple {
|
||||
|
||||
}
|
||||
|
||||
// UI: complex version of the outline
|
||||
.outline-complex {
|
||||
|
||||
.outline-content {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// outline: items
|
||||
.outline-item {
|
||||
|
||||
// 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 {
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
@include transition-property(none);
|
||||
}
|
||||
}
|
||||
|
||||
// item: title
|
||||
.item-title {
|
||||
|
||||
// STATE: is-editable
|
||||
&.is-editable {
|
||||
|
||||
// editor
|
||||
+ .editor {
|
||||
display: block;
|
||||
|
||||
.item-edit-title {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop
|
||||
.drop-target-prepend .draggable-drop-indicator-initial {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// STATE: was dropped
|
||||
&.was-dropped {
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: sections
|
||||
// --------------------
|
||||
.outline-section {
|
||||
padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4);
|
||||
|
||||
// header
|
||||
.section-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-strong;
|
||||
@extend %t-title5;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-section-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-section-title {
|
||||
width: flex-grid(5, 6);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
|
||||
// in-context actions
|
||||
.incontext-editor-action-wrapper {
|
||||
top: -($baseline/20);
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
margin: 0 0 0 ($outline-indent-width*1.25);
|
||||
}
|
||||
|
||||
// content
|
||||
.section-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.section-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop - was dropped
|
||||
&.was-dropped {
|
||||
border-left-color: $ui-action-primary-color-focus;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
// --------------------
|
||||
.list-subsections {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-subsection {
|
||||
padding: ($baseline*0.75);
|
||||
|
||||
// header
|
||||
.subsection-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-title6;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-subsection-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-subsection-title {
|
||||
width: flex-grid(5, 6);
|
||||
margin-top: -($baseline/10);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// in-context actions
|
||||
.incontext-editor-action-wrapper {
|
||||
top: -($baseline/10);
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
margin: 0 0 0 $outline-indent-width;
|
||||
}
|
||||
|
||||
// content
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon4;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
// --------------------
|
||||
.list-units {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-unit {
|
||||
@include transition(margin $tmg-f2 linear 0s); // needed for drag and drop transitions
|
||||
margin-left: $outline-indent-width;
|
||||
|
||||
// header
|
||||
.unit-header {
|
||||
@extend %outline-item-header;
|
||||
}
|
||||
|
||||
.unit-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
|
||||
.unit-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/10);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add/new items
|
||||
.add-item {
|
||||
margin-top: ($baseline*0.75);
|
||||
@@ -256,258 +515,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// outline: items
|
||||
.outline-item {
|
||||
|
||||
// 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 {
|
||||
|
||||
// STATE: hover/active
|
||||
&:hover, &:active {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging {
|
||||
@include transition-property(none);
|
||||
}
|
||||
}
|
||||
|
||||
// item: title
|
||||
.item-title {
|
||||
|
||||
// STATE: is-editable
|
||||
&.is-editable {
|
||||
|
||||
// editor
|
||||
+ .editor {
|
||||
display: block;
|
||||
|
||||
.item-edit-title {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop
|
||||
.drop-target-prepend .draggable-drop-indicator-initial {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
// STATE: was dropped
|
||||
&.was-dropped {
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: sections
|
||||
// --------------------
|
||||
.outline-section {
|
||||
padding: ($baseline*0.75) $baseline ($baseline*0.75) ($baseline + 4);
|
||||
|
||||
// header
|
||||
.section-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-strong;
|
||||
@extend %t-title5;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-section-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-section-title {
|
||||
width: flex-grid(5, 6);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
|
||||
// in-context actions
|
||||
.incontext-editor-action-wrapper {
|
||||
top: -($baseline/20);
|
||||
}
|
||||
|
||||
// status
|
||||
.section-status {
|
||||
margin: 0 0 0 ($outline-indent-width*1.25);
|
||||
}
|
||||
|
||||
// content
|
||||
.section-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon3;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.section-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: drag and drop - was dropped
|
||||
&.was-dropped {
|
||||
border-left-color: $ui-action-primary-color-focus;
|
||||
}
|
||||
}
|
||||
|
||||
// outline: subsections
|
||||
// --------------------
|
||||
.list-subsections {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-subsection {
|
||||
padding: ($baseline*0.75);
|
||||
|
||||
// header
|
||||
.subsection-header {
|
||||
@extend %outline-item-header;
|
||||
|
||||
.incontext-editor-input {
|
||||
@extend %t-title6;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
|
||||
.icon, .wrapper-subsection-title {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: ($baseline/4);
|
||||
}
|
||||
|
||||
.wrapper-subsection-title {
|
||||
width: flex-grid(5, 6);
|
||||
margin-top: -($baseline/10);
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.subsection-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/4);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
margin-right: ($baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// in-context actions
|
||||
.incontext-editor-action-wrapper {
|
||||
top: -($baseline/10);
|
||||
}
|
||||
|
||||
// status
|
||||
.subsection-status {
|
||||
margin: 0 0 0 $outline-indent-width;
|
||||
}
|
||||
|
||||
// content
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-shown;
|
||||
}
|
||||
|
||||
// CASE: is-collapsible
|
||||
&.is-collapsible {
|
||||
@extend %ui-expand-collapse;
|
||||
|
||||
.ui-toggle-expansion {
|
||||
@extend %t-icon4;
|
||||
color: $gray-l3;
|
||||
}
|
||||
}
|
||||
|
||||
// STATE: is-collapsed
|
||||
&.is-collapsed {
|
||||
|
||||
.subsection-content {
|
||||
@extend %outline-item-content-hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// outline: units
|
||||
// --------------------
|
||||
.list-units {
|
||||
margin: $baseline 0 0 0;
|
||||
}
|
||||
|
||||
.outline-unit {
|
||||
@include transition(margin $tmg-f2 linear 0s); // needed for drag and drop transitions
|
||||
margin-left: $outline-indent-width;
|
||||
|
||||
// header
|
||||
.unit-header {
|
||||
@extend %outline-item-header;
|
||||
}
|
||||
|
||||
.unit-header-details {
|
||||
float: left;
|
||||
width: flex-grid(6, 9);
|
||||
margin-top: ($baseline/4);
|
||||
}
|
||||
|
||||
.unit-header-actions {
|
||||
float: right;
|
||||
width: flex-grid(3, 9);
|
||||
margin-top: -($baseline/10);
|
||||
text-align: right;
|
||||
|
||||
.actions-list {
|
||||
@extend %actions-list;
|
||||
@extend %t-action2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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>
|
||||
|
||||
7
cms/templates/js/course-outline-modal.underscore
Normal file
7
cms/templates/js/course-outline-modal.underscore
Normal file
@@ -0,0 +1,7 @@
|
||||
<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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<%
|
||||
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">
|
||||
|
||||
22
cms/templates/js/due-date-editor.underscore
Normal file
22
cms/templates/js/due-date-editor.underscore
Normal file
@@ -0,0 +1,22 @@
|
||||
<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>
|
||||
@@ -1,125 +0,0 @@
|
||||
<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>
|
||||
14
cms/templates/js/grading-editor.underscore
Normal file
14
cms/templates/js/grading-editor.underscore
Normal file
@@ -0,0 +1,14 @@
|
||||
<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>
|
||||
38
cms/templates/js/publish-editor.underscore
Normal file
38
cms/templates/js/publish-editor.underscore
Normal file
@@ -0,0 +1,38 @@
|
||||
<% 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>
|
||||
<% } %>
|
||||
28
cms/templates/js/release-date-editor.underscore
Normal file
28
cms/templates/js/release-date-editor.underscore
Normal file
@@ -0,0 +1,28 @@
|
||||
<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>
|
||||
35
cms/templates/js/staff-lock-editor.underscore
Normal file
35
cms/templates/js/staff-lock-editor.underscore
Normal file
@@ -0,0 +1,35 @@
|
||||
<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>
|
||||
143
cms/templates/ux/reference/modal_bulkpublish-section.html
Normal file
143
cms/templates/ux/reference/modal_bulkpublish-section.html
Normal file
@@ -0,0 +1,143 @@
|
||||
<%! 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>
|
||||
73
cms/templates/ux/reference/modal_bulkpublish-subsection.html
Normal file
73
cms/templates/ux/reference/modal_bulkpublish-subsection.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<%! 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>
|
||||
30
cms/templates/ux/reference/modal_bulkpublish-unit.html
Normal file
30
cms/templates/ux/reference/modal_bulkpublish-unit.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<%! 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)
|
||||
|
||||
Reference in New Issue
Block a user