Apply code review comments and fix tests

This commit is contained in:
Andy Armstrong
2014-07-25 17:10:43 -04:00
committed by cahrens
parent 9b98604975
commit ef581e1146
29 changed files with 366 additions and 297 deletions

View File

@@ -45,14 +45,15 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
*/
"published_by": null,
/**
* Represents the possible publish states for an xblock:
* is_live - the block and all of its children are live to students (except for staff only items)
* is_ready - the block and all of its children are ready to go live in the future
* unscheduled - the block and all of its children are unscheduled
* has_unpublished_content - the block or its children have unpublished content that is not staff only
* is_staff_only - all of the block's content is to be shown to staff only
* True if the xblock has changes.
* Note: this is not always provided as a performance optimization.
*/
"publish_state": null,
"has_changes": null,
/**
* Represents the possible publish states for an xblock. See the documentation
* for XBlockVisibility to see a comprehensive enumeration of the states.
*/
"visibility_state": null,
/**
* True iff the release date of the xblock is in the past.
*/

View File

@@ -1,7 +1,9 @@
define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sinon", "js/spec_helpers/edit_helpers",
"js/views/feedback_prompt", "js/views/pages/container", "js/views/pages/container_subviews",
"js/models/xblock_info"],
function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, ContainerSubviews, XBlockInfo) {
"js/models/xblock_info", "js/views/utils/xblock_utils"],
function ($, _, str, create_sinon, edit_helpers, Prompt, ContainerPage, ContainerSubviews,
XBlockInfo, XBlockUtils) {
var VisibilityState = XBlockUtils.VisibilityState;
describe("Container Subviews", function() {
var model, containerPage, requests, createContainerPage, renderContainerPage,
@@ -23,7 +25,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical',
publish_state: 'unscheduled',
published: false,
has_changes: false,
visibility_state: 'unscheduled',
edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe",
published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako",
currently_visible_to_students: false
@@ -85,22 +89,23 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('updates when publish state changes', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({publish_state: 'ready'});
fetch({published: true});
expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss);
fetch({publish_state: 'unscheduled'});
fetch({published: false});
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
});
it('updates when has_changes attribute changes', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({publish_state: 'has-unpublished-changes'});
fetch({has_changes: true});
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
fetch({publish_state: 'ready'});
fetch({published: true, has_changes: false});
expect(containerPage.$(previewCss)).toHaveClass(disabledCss);
fetch({publish_state: 'unscheduled'});
// If published is false, preview is always enabled.
fetch({published: false, has_changes: false});
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
});
});
@@ -123,7 +128,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
// Helper function to do the discard operation, up until the server response.
containerPage.render();
respondWithHtml(mockContainerXBlockHtml);
fetch({publish_state: 'has_unpublished_content'});
fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention});
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
// Click discard changes
@@ -151,26 +156,29 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
};
renderContainerPage(this, mockContainerXBlockHtml);
fetch({publishState: 'unscheduled'});
fetch({published: false, has_changes: false});
verifyPrivateState();
fetch({published: false, has_changes: true});
verifyPrivateState();
});
it('renders correctly with published content', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({publish_state: 'ready'});
fetch({published: true, has_changes: false, visibility_state: VisibilityState.ready});
expect(containerPage.$(headerCss).text()).toContain('Published');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
fetch({publish_state: 'has_unpublished_content'});
fetch({published: true, has_changes: true, visibility_state: VisibilityState.needsAttention});
expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(hasWarningsClass);
fetch({publish_state: 'live'});
expect(containerPage.$(headerCss).text()).toContain('Published');
fetch({published: true, has_changes: false, visibility_state: VisibilityState.live});
expect(containerPage.$(headerCss).text()).toContain('Published and Live');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(liveClass);
@@ -179,7 +187,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('can publish private content', function () {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(hasWarningsClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(liveClass);
// Click publish
containerPage.$(publishButtonCss).click();
@@ -195,15 +205,17 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
create_sinon.expectJsonRequest(requests, "GET", "/xblock/locator-container");
// Response to fetch
respondWithJson(createXBlockInfo({publish_state: 'ready'}));
respondWithJson(createXBlockInfo({
published: true, has_changes: false, visibility_state: VisibilityState.ready
}));
// Verify updates displayed
expect(containerPage.$(bitPublishingCss)).toHaveClass(readyClass);
// Verify that the "publish_state" value has been updated
expect(containerPage.model.get("publish_state")).toBe('ready');
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
});
it('can does not fetch if publish fails', function () {
it('does not refresh if publish fails', function () {
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
@@ -218,8 +230,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
// Verify still in draft state.
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
// Verify that the "publish_state" value has been updated
expect(containerPage.model.get("publish_state")).toBe('unscheduled');
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
});
it('can discard changes', function () {
@@ -260,7 +272,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('does not discard changes on cancel', function () {
renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'has_unpublished_content' });
renderContainerPage(this, mockContainerXBlockHtml);
fetch({published: true, has_changes: true});
var numRequests = requests.length;
// Click discard changes
@@ -284,10 +297,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('renders the last saved date and user when there are changes', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
publish_state: 'has_unpublished_content',
edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe"
});
fetch({has_changes: true, edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe"});
expect(containerPage.$(lastDraftCss).text()).
toContain("Draft saved on Jul 02, 2014 at 14:20 UTC by joe");
});
@@ -296,7 +306,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('renders correctly when unreleased', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
publish_state: 'ready', released_to_students: false,
visibility_state: VisibilityState.ready, released_to_students: false,
release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"'
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Scheduled:");
@@ -307,7 +317,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('renders correctly when released', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
publish_state: 'live', released_to_students: true,
visibility_state: VisibilityState.live, released_to_students: true,
release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"'
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Released:");
@@ -318,7 +328,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('renders correctly when the release date is not set', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
publish_state: 'unscheduled', "released_to_students": false,
visibility_state: VisibilityState.unscheduled, "released_to_students": false,
release_date: null, release_date_from: null
});
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
@@ -328,7 +338,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('renders correctly when the unit is not published', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
publish_state: 'has_unpublished_content', released_to_students: true,
visibility_state: VisibilityState.needsAttention, released_to_students: true,
release_date: "Jul 02, 2014 at 14:20 UTC", release_date_from: 'Section "Week 1"'
});
containerPage.xblockPublisher.render();
@@ -362,7 +372,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
create_sinon.respondWithJson(requests, createXBlockInfo({
publish_state: isStaffOnly ? 'staff_only' : 'unscheduled'
published: containerPage.model.get('published'),
visibility_state: isStaffOnly ? VisibilityState.staffOnly : VisibilityState.live
}));
};
@@ -385,14 +396,15 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it("can be set to staff only", function() {
renderContainerPage(this, mockContainerXBlockHtml);
containerPage.$('.action-staff-lock').click();
requestStaffOnly(true);
verifyStaffOnly(true);
});
it("can remove staff only setting", function() {
promptSpy = edit_helpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'staff_only' });
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly
});
requestStaffOnly(false);
verifyStaffOnly(false);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(readyClass);
@@ -401,7 +413,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it("does not refresh if removing staff only is canceled", function() {
var requestCount;
promptSpy = edit_helpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, { publish_state: 'staff_only' });
renderContainerPage(this, mockContainerXBlockHtml, {
visibility_state: VisibilityState.staffOnly
});
requestCount = requests.length;
containerPage.$('.action-staff-lock').click();
edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel

View File

@@ -118,7 +118,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
category: 'vertical',
studio_url: '/container/mock-unit',
is_container: true,
publish_state: 'unscheduled',
visibility_state: 'unscheduled',
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser'
}])
@@ -432,9 +432,5 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect(unitAnchor.attr('href')).toBe('/container/mock-unit');
});
});
describe("Publishing State", function() {
// TODO: implement this!!!!
});
});
});

View File

@@ -25,14 +25,14 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
category: 'vertical',
display_name: displayName,
studio_url: '/container/mock-unit',
publish_state: 'unscheduled',
visibility_state: 'unscheduled',
ancestor_info: {
ancestors: [{
id: 'mock-subsection',
category: 'sequential',
display_name: 'Mock Subsection',
studio_url: '/course/mock-course?show=mock-subsection',
publish_state: 'unscheduled',
visibility_state: 'unscheduled',
child_info: {
category: 'vertical',
display_name: 'Unit',
@@ -41,13 +41,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
category: 'vertical',
display_name: displayName,
studio_url: '/container/mock-unit',
publish_state: 'unscheduled'
visibility_state: 'unscheduled'
}, {
id: 'mock-unit-2',
category: 'vertical',
display_name: 'Mock Unit 2',
studio_url: '/container/mock-unit-2',
publish_state: 'unscheduled'
visibility_state: 'unscheduled'
}]
}
}, {
@@ -55,13 +55,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
category: 'chapter',
display_name: 'Section',
studio_url: '/course/slashes:mock-course?show=mock-section',
publish_state: 'unscheduled'
visibility_state: 'unscheduled'
}, {
id: 'mock-course',
category: 'course',
display_name: 'Mock Course',
studio_url: '/course/mock-course',
publish_state: 'unscheduled'
visibility_state: 'unscheduled'
}]
},
metadata: {

View File

@@ -17,6 +17,9 @@ define(["jquery", "underscore", "backbone", "gettext", "js/utils/handle_iframe_b
},
options: {
// UX is moving towards using 'is-collapsed' in preference over 'collapsed',
// but use the old scheme as the default so that existing code doesn't need
// to be rewritten.
collapsedClass: 'collapsed'
},

View File

@@ -1,10 +1,11 @@
/**
* Subviews (usually small side panels) for XBlockContainerPage.
*/
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/view_utils"],
function ($, _, gettext, BaseView, ViewUtils) {
var disabledCss = "is-disabled";
define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/view_utils",
"js/views/utils/xblock_utils"],
function ($, _, gettext, BaseView, ViewUtils, XBlockViewUtils) {
var VisibilityState = XBlockViewUtils.VisibilityState,
disabledCss = "is-disabled";
/**
* A view that refreshes the view when certain values in the XBlockInfo have changed
@@ -53,20 +54,19 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
*/
var PreviewActionController = ContainerStateListenerView.extend({
shouldRefresh: function(model) {
return ViewUtils.hasChangedAttributes(model, ['edited_on', 'published_on', 'publish_state']);
return ViewUtils.hasChangedAttributes(model, ['has_changes', 'published']);
},
render: function() {
var previewAction = this.$el.find('.button-preview'),
viewLiveAction = this.$el.find('.button-view'),
publishState = this.model.get('publish_state');
if (publishState !== 'unscheduled') {
viewLiveAction = this.$el.find('.button-view');
if (this.model.get('published')) {
viewLiveAction.removeClass(disabledCss);
}
else {
viewLiveAction.addClass(disabledCss);
}
if (publishState !== 'live' && publishState !== 'ready') {
if (this.model.get('has_changes') || !this.model.get('published')) {
previewAction.removeClass(disabledCss);
}
else {
@@ -99,14 +99,18 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, [ 'edited_on', 'published_on', 'publish_state' ])) {
if (ViewUtils.hasChangedAttributes(model, [
'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state'
])) {
this.render();
}
},
render: function () {
this.$el.html(this.template({
publishState: this.model.get('publish_state'),
visibilityState: this.model.get('visibility_state'),
visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(this.model.get('visibility_state')),
hasChanges: this.model.get('has_changes'),
editedOn: this.model.get('edited_on'),
editedBy: this.model.get('edited_by'),
published: this.model.get('published'),
@@ -162,7 +166,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
if (e && e.preventDefault) {
e.preventDefault();
}
enableStaffLock = xblockInfo.get('publish_state') !== 'staff_only';
enableStaffLock = xblockInfo.get('visibility_state') !== VisibilityState.staffOnly;
revertCheckBox = function() {
self.checkStaffLock(!enableStaffLock);
@@ -221,7 +225,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, ['published_on'])) {
if (ViewUtils.hasChangedAttributes(model, ['published', 'published_on', 'published_by'])) {
this.render();
}
},

View File

@@ -10,7 +10,6 @@ define(['js/views/xblock_outline'],
// takes XBlockInfo as a model
templateName: 'unit-outline',
className: 'group-configurations-list',
render: function() {
XBlockOutlineView.prototype.render.call(this);

View File

@@ -3,7 +3,36 @@
*/
define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/utils/module"],
function($, _, gettext, ViewUtils, ModuleUtils) {
var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField;
var addXBlock, deleteXBlock, createUpdateRequestData, updateXBlockField, VisibilityState,
getXBlockVisibilityClass;
/**
* Represents the possible visibility states for an xblock:
*
* live - the block and all of its descendants are live to students (excluding staff only)
* Note: Live means both published and released.
*
* ready - the block is ready to go live and all of its descendants are live or ready (excluding staff only)
* Note: content is ready when it is published and scheduled with a release date in the future.
*
* unscheduled - the block and all of its descendants have no release date (excluding staff only)
* Note: it is valid for items to be published with no release date in which case they are unscheduled.
*
* needsAttention - the block or its descendants need attention
* i.e. there is some content that is not fully live, ready, unscheduled or staff only.
* For example: one subsection has draft content, or there's both unreleased and released content
* in one section.
*
* staffOnly - all of the block's content is to be shown to staff only
* Note: staff only items do not affect their parent's state.
*/
VisibilityState = {
live: 'live',
ready: 'ready',
unscheduled: 'unscheduled',
needsAttention: 'needs_attention',
staffOnly: 'staff_only'
};
/**
* Adds an xblock based upon the data attributes of the specified add button. A promise
@@ -83,9 +112,30 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
});
};
/**
* Returns the CSS class to represent the specified xblock visibility state.
*/
getXBlockVisibilityClass = function(visibilityState) {
if (visibilityState === VisibilityState.staffOnly) {
return 'is-staff-only';
}
if (visibilityState === VisibilityState.live) {
return 'is-live';
}
if (visibilityState === VisibilityState.ready) {
return 'is-ready';
}
if (visibilityState === VisibilityState.needsAttention) {
return 'has-warnings';
}
return '';
};
return {
'VisibilityState': VisibilityState,
'addXBlock': addXBlock,
'deleteXBlock': deleteXBlock,
'updateXBlockField': updateXBlockField
'updateXBlockField': updateXBlockField,
'getXBlockVisibilityClass': getXBlockVisibilityClass
};
});

View File

@@ -68,6 +68,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
}
html = this.template({
xblockInfo: xblockInfo,
visibilityClass: XBlockViewUtils.getXBlockVisibilityClass(xblockInfo.get('visibility_state')),
parentInfo: this.parentInfo,
xblockType: xblockType,
parentType: parentType,
@@ -200,7 +201,11 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
} else {
locatorElement = this.$('.outline-item[data-locator="' + locatorToShow + '"]');
}
ViewUtils.setScrollOffset(locatorElement, scrollOffset);
if (locatorElement.length > 0) {
ViewUtils.setScrollOffset(locatorElement, scrollOffset);
} else {
console.error("Failed to show item with locator " + locatorToShow + "");
}
if (editDisplayName) {
locatorElement.find('> div[class$="header"] .xblock-field-value-edit').click();
}