Support staff locking on the unit page

STUD-1873
This commit is contained in:
cahrens
2014-07-10 11:33:49 -04:00
parent cf70eb6e2a
commit d447c07540
23 changed files with 926 additions and 424 deletions

View File

@@ -38,7 +38,7 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
* If true, only course staff can see the xblock regardless of publish status or
* release date status.
*/
"locked": null,
"visible_to_staff_only": null,
/**
* Date of the last edit to this xblock or any of its descendants.
*/
@@ -69,7 +69,12 @@ define(["backbone", "underscore", "js/utils/module"], function(Backbone, _, Modu
* this will either be the parent subsection or the grandparent section.
* This can be null if the release date is unscheduled.
*/
"release_date_from":null
"release_date_from":null,
/**
* True if this xblock is currently visible to students. This is computed server-side
* so that the logic isn't duplicated on the client.
*/
"currently_visible_to_students": null
},
parse: function(response) {

View File

@@ -48,7 +48,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
);
};
renderContainerPage = function(html, test, options) {
renderContainerPage = function(test, html, options) {
requests = create_sinon.requests(test);
containerPage = new ContainerPage(_.extend(options || {}, {
model: model,
@@ -70,7 +70,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
describe("Initial display", function() {
it('can render itself', function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$('.xblock-header').length).toBe(9);
expect(containerPage.$('.wrapper-xblock .level-nesting')).not.toHaveClass('is-hidden');
});
@@ -84,7 +84,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('inline edits the display name when performing a new action', function() {
renderContainerPage(mockContainerXBlockHtml, this, {
renderContainerPage(this, mockContainerXBlockHtml, {
action: 'new'
});
expect(containerPage.$('.xblock-header').length).toBe(9);
@@ -106,8 +106,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
expectEditCanceled = function(test, options) {
var initialRequests, displayNameWrapper;
renderContainerPage(mockContainerXBlockHtml, test);
var initialRequests, displayNameWrapper, displayNameInput;
renderContainerPage(test, mockContainerXBlockHtml);
displayNameWrapper = getDisplayNameWrapper();
initialRequests = requests.length;
displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, options.newTitle);
@@ -125,7 +125,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('can edit itself', function() {
var editButtons, displayNameElement;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
displayNameElement = containerPage.$('.page-header-title');
// Click the root edit button
@@ -162,7 +162,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('can inline edit the display name', function() {
var displayNameInput, displayNameWrapper;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
displayNameWrapper = getDisplayNameWrapper();
displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName);
displayNameInput.change();
@@ -176,7 +176,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('does not change the title when a display name update fails', function() {
var initialRequests, displayNameInput, displayNameWrapper;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
displayNameWrapper = getDisplayNameWrapper();
displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName);
initialRequests = requests.length;
@@ -190,7 +190,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('trims whitespace from the display name', function() {
var displayNameInput, displayNameWrapper;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
displayNameWrapper = getDisplayNameWrapper();
displayNameInput = edit_helpers.inlineEdit(displayNameWrapper, updatedDisplayName + ' ');
displayNameInput.change();
@@ -222,7 +222,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('can show an edit modal for a child xblock', function() {
var editButtons;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
@@ -258,7 +258,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('can save changes to settings', function() {
var editButtons, modal, mockUpdatedXBlockHtml;
mockUpdatedXBlockHtml = readFixtures('mock/mock-updated-xblock.underscore');
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
editButtons = containerPage.$('.wrapper-xblock .edit-button');
// The container should have rendered six mock xblocks
expect(editButtons.length).toBe(6);
@@ -346,24 +346,24 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
it("can delete the first xblock", function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(0);
});
it("can delete a middle xblock", function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(1);
});
it("can delete the last xblock", function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
deleteComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it('does not delete when clicking No in prompt', function () {
var numRequests;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
numRequests = requests.length;
// click delete on the first component but press no
@@ -378,7 +378,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('shows a notification during the delete operation', function() {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
clickDelete(0);
edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
create_sinon.respondWithJson(requests, {});
@@ -387,7 +387,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('does not delete an xblock upon failure', function () {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
clickDelete(0);
edit_helpers.verifyNotificationShowing(notificationSpy, /Deleting/);
create_sinon.respondWithError(requests);
@@ -431,23 +431,23 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
it("can duplicate the first xblock", function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(0);
});
it("can duplicate a middle xblock", function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(1);
});
it("can duplicate the last xblock", function() {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
duplicateComponentWithSuccess(NUM_COMPONENTS_PER_GROUP - 1);
});
it('shows a notification when duplicating', function () {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
clickDuplicate(0);
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
create_sinon.respondWithJson(requests, {"locator": "new_item"});
@@ -456,7 +456,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('does not duplicate an xblock upon failure', function () {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
refreshXBlockSpies = spyOn(containerPage, "refreshXBlock");
clickDuplicate(0);
edit_helpers.verifyNotificationShowing(notificationSpy, /Duplicating/);
@@ -475,7 +475,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
it('sends the correct JSON to the server', function () {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
edit_helpers.verifyXBlockRequest(requests, {
"category": "discussion",
@@ -486,7 +486,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('shows a notification while creating', function () {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
edit_helpers.verifyNotificationShowing(notificationSpy, /Adding/);
create_sinon.respondWithJson(requests, { });
@@ -495,7 +495,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('does not insert component upon failure', function () {
var requestCount;
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
clickNewComponent(0);
requestCount = requests.length;
create_sinon.respondWithError(requests);
@@ -514,7 +514,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
verifyCreateHtmlComponent = function(test, templateIndex, expectedRequest) {
var xblockCount;
renderContainerPage(mockContainerXBlockHtml, test);
renderContainerPage(test, mockContainerXBlockHtml);
showTemplatePicker();
xblockCount = containerPage.$('.studio-xblock-wrapper').length;
containerPage.$('.new-component-html a')[templateIndex].click();

View File

@@ -1,11 +1,12 @@
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"],
"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) {
describe("Container Subviews", function() {
var model, containerPage, requests, renderContainerPage, respondWithHtml, respondWithJson, fetch,
disabledCss = "is-disabled",
var model, containerPage, requests, createContainerPage, renderContainerPage,
respondWithHtml, respondWithJson, fetch,
disabledCss = "is-disabled", defaultXBlockInfo, createXBlockInfo,
mockContainerPage = readFixtures('mock/mock-container-page.underscore'),
mockContainerXBlockHtml = readFixtures('mock/mock-empty-container-xblock.underscore');
@@ -14,27 +15,39 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
edit_helpers.installTemplate('publish-xblock');
edit_helpers.installTemplate('publish-history');
edit_helpers.installTemplate('unit-outline');
edit_helpers.installTemplate('container-message');
appendSetFixtures(mockContainerPage);
});
model = new XBlockInfo({
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical',
published: false,
has_changes: false
}, {
parse: true
});
defaultXBlockInfo = {
id: 'locator-container',
display_name: 'Test Container',
category: 'vertical',
published: false,
has_changes: false,
edited_on: "Jul 02, 2014 at 14:20 UTC", edited_by: "joe",
published_on: "Jul 01, 2014 at 12:45 UTC", published_by: "amako",
visible_to_staff_only: false,
currently_visible_to_students: false
};
createXBlockInfo = function(options) {
return _.extend(_.extend({}, defaultXBlockInfo), options || {});
};
createContainerPage = function (test, options) {
requests = create_sinon.requests(test);
model = new XBlockInfo(createXBlockInfo(options), { parse: true });
containerPage = new ContainerPage({
model: model,
templates: edit_helpers.mockComponentTemplates,
el: $('#content'),
isUnitPage: true
});
});
};
renderContainerPage = function(html, that) {
requests = create_sinon.requests(that);
renderContainerPage = function (test, html, options) {
createContainerPage(test, options);
containerPage.render();
respondWithHtml(html);
};
@@ -57,6 +70,7 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
};
fetch = function (json) {
json = createXBlockInfo(json);
model.fetch();
respondWithJson(json);
};
@@ -66,30 +80,30 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
previewCss = '.button-preview';
it('renders correctly for private unit', function () {
renderContainerPage(mockContainerXBlockHtml, this);
renderContainerPage(this, mockContainerXBlockHtml);
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
});
it('updates when published attribute changes', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": true});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": true});
expect(containerPage.$(viewPublishedCss)).not.toHaveClass(disabledCss);
fetch({"id": "locator-container", "published": false});
fetch({"published": false});
expect(containerPage.$(viewPublishedCss)).toHaveClass(disabledCss);
});
it('updates when has_changes attribute changes', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "has_changes": true});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"has_changes": true});
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
fetch({"id": "locator-container", "published": true, "has_changes": false});
fetch({"published": true, "has_changes": false});
expect(containerPage.$(previewCss)).toHaveClass(disabledCss);
// If published is false, preview is always enabled.
fetch({"id": "locator-container", "published": false, "has_changes": false});
fetch({"published": false, "has_changes": false});
expect(containerPage.$(previewCss)).not.toHaveClass(disabledCss);
});
});
@@ -97,21 +111,21 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
describe("Publisher", function () {
var headerCss = '.pub-status',
bitPublishingCss = "div.bit-publishing",
publishedBit = "published",
draftBit = "draft",
publishedBit = "is-published",
draftBit = "is-draft",
staffOnlyBit = "is-staff-only",
publishButtonCss = ".action-publish",
discardChangesButtonCss = ".action-discard",
lastDraftCss = ".wrapper-last-draft",
releaseDateTitleCss = ".wrapper-release .title",
releaseDateContentCss = ".wrapper-release .copy",
lastRequest, promptSpies, sendDiscardChangesToServer;
promptSpies, sendDiscardChangesToServer;
lastRequest = function() { return requests[requests.length - 1]; };
sendDiscardChangesToServer = function(test) {
sendDiscardChangesToServer = function() {
// Helper function to do the discard operation, up until the server response.
renderContainerPage(mockContainerXBlockHtml, test);
fetch({"id": "locator-container", "published": true, "has_changes": true});
containerPage.render();
respondWithHtml(mockContainerXBlockHtml);
fetch({"published": true, "has_changes": true});
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass('is-disabled');
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
// Click discard changes
@@ -132,30 +146,30 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('renders correctly with private content', function () {
var verifyPrivateState = function(){
// State is the same regardless of "has_changes" value.
expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)');
var verifyPrivateState = function() {
expect(containerPage.$(headerCss).text()).toContain('Draft (Never published)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(draftBit);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit);
};
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": false, "has_changes": false});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": false, "has_changes": false});
verifyPrivateState();
fetch({"id": "locator-container", "published": false, "has_changes": true});
fetch({"published": false, "has_changes": true});
verifyPrivateState();
});
it('renders correctly with public content', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": true, "has_changes": false});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": true, "has_changes": false});
expect(containerPage.$(headerCss).text()).toContain('Published');
expect(containerPage.$(publishButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).toHaveClass(disabledCss);
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
fetch({"id": "locator-container", "published": true, "has_changes": true});
fetch({"published": true, "has_changes": true});
expect(containerPage.$(headerCss).text()).toContain('Draft (Unpublished changes)');
expect(containerPage.$(publishButtonCss)).not.toHaveClass(disabledCss);
expect(containerPage.$(discardChangesButtonCss)).not.toHaveClass(disabledCss);
@@ -164,9 +178,10 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
it('can publish private content', function () {
var notificationSpy = edit_helpers.createNotificationSpy();
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": false, "has_changes": false});
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": false, "has_changes": false});
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(draftBit);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit);
// Click publish
containerPage.$(publishButtonCss).click();
@@ -191,9 +206,9 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('can does not fetch if publish fails', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": false, "has_changes": false});
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": false});
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit);
// Click publish
containerPage.$(publishButtonCss).click();
@@ -205,17 +220,18 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
expect(requests.length).toEqual(numRequests);
// Verify still in draft state.
expect(containerPage.$(bitPublishingCss)).toHaveClass(draftBit);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit);
// Verify that the "published" value has been cleared out of the model.
expect(containerPage.model.get("publish")).toBeNull();
});
it('can discard changes', function () {
var notificationSpy = edit_helpers.createNotificationSpy(),
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').andCallThrough(),
numRequests;
var notificationSpy, renderPageSpy, numRequests;
createContainerPage(this);
notificationSpy = edit_helpers.createNotificationSpy();
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').andCallThrough();
sendDiscardChangesToServer(this);
sendDiscardChangesToServer();
numRequests = requests.length;
// Respond with success.
@@ -230,10 +246,11 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('does not fetch if discard changes fails', function () {
var renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').andCallThrough(),
numRequests;
var renderPageSpy, numRequests;
createContainerPage(this);
renderPageSpy = spyOn(containerPage.xblockPublisher, 'renderPage').andCallThrough();
sendDiscardChangesToServer(this);
sendDiscardChangesToServer();
numRequests = requests.length;
// Respond with failure
@@ -246,8 +263,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('does not discard changes on cancel', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({"id": "locator-container", "published": true, "has_changes": true});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": true, "has_changes": true});
var numRequests = requests.length;
// Click discard changes
@@ -262,84 +279,213 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
});
it('renders the last published date and user when there are no changes', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "has_changes": false,
"edited_on": "Jun 30, 2014 at 14:20 UTC", "edited_by": "joe",
"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"});
expect(containerPage.$(lastDraftCss).text()).
toContain("Last published Jul 01, 2014 at 12:45 UTC by amako");
});
it('renders the last saved date and user when there are changes', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "has_changes": true,
"edited_on": "Jul 02, 2014 at 14:20 UTC", "edited_by": "joe",
"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"});
renderContainerPage(this, mockContainerXBlockHtml);
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");
});
it('renders the release date correctly when unreleased', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": true, "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:");
expect(containerPage.$(releaseDateContentCss).text()).
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
describe("Release Date", function() {
it('renders correctly when unreleased', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": true, "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:");
expect(containerPage.$(releaseDateContentCss).text()).
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
});
it('renders correctly when released', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": true, "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:");
expect(containerPage.$(releaseDateContentCss).text()).
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
});
it('renders correctly when the release date is not set', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": true, "released_to_students": false,
"release_date": null, "release_date_from": null });
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
expect(containerPage.$(releaseDateContentCss).text()).toContain("Unscheduled");
});
it('renders correctly when the unit is not published', function () {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({"published": false, "released_to_students": true,
"release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' });
// Force a render because none of the fetched fields will trigger a render
containerPage.xblockPublisher.render();
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
expect(containerPage.$(releaseDateContentCss).text()).
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
});
});
it('renders the release date correctly when released', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": true, "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:");
expect(containerPage.$(releaseDateContentCss).text()).
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
});
describe("Content Visibility", function () {
var requestStaffOnly, verifyStaffOnly, promptSpy;
it('renders the release date correctly when the release date is not set', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": true, "released_to_students": false,
"release_date": null, "release_date_from": null });
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
expect(containerPage.$(releaseDateContentCss).text()).toContain("Unscheduled");
});
requestStaffOnly = function(isStaffOnly) {
containerPage.$('.action-staff-lock').click();
it('renders the release date correctly when the unit is not published', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": false, "released_to_students": true,
"release_date": "Jul 02, 2014 at 14:20 UTC", "release_date_from": 'Section "Week 1"' });
// Force a render because none of the fetched fields will trigger a render
containerPage.xblockPublisher.render();
expect(containerPage.$(releaseDateTitleCss).text()).toContain("Release:");
expect(containerPage.$(releaseDateContentCss).text()).
toContain('Jul 02, 2014 at 14:20 UTC with Section "Week 1"');
// If removing the staff lock, click 'Yes' to confirm
if (!isStaffOnly) {
edit_helpers.confirmPrompt(promptSpy);
}
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/locator-container', {
publish: 'republish',
metadata: { visible_to_staff_only: isStaffOnly }
});
create_sinon.respondWithJson(requests, {
data: null,
id: "locator-container",
metadata: {
visible_to_staff_only: isStaffOnly
}
});
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
create_sinon.respondWithJson(requests, createXBlockInfo({
published: containerPage.model.get('published'),
visible_to_staff_only: isStaffOnly
}));
};
verifyStaffOnly = function(isStaffOnly) {
if (isStaffOnly) {
expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check');
expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff Only');
expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyBit);
} else {
expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check-empty');
expect(containerPage.$('.wrapper-visibility .copy').text()).toBe('Staff and Students');
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyBit);
}
};
it("is initially shown to all", function() {
renderContainerPage(this, mockContainerXBlockHtml);
verifyStaffOnly(false);
});
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);
requestStaffOnly(true);
requestStaffOnly(false);
verifyStaffOnly(false);
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(publishedBit);
});
it("can remove staff only setting from published unit", function() {
promptSpy = edit_helpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml, { published: true });
requestStaffOnly(true);
requestStaffOnly(false);
verifyStaffOnly(false);
expect(containerPage.$(bitPublishingCss)).toHaveClass(publishedBit);
});
it("does not refresh if removing staff only is canceled", function() {
var requestCount;
promptSpy = edit_helpers.createPromptSpy();
renderContainerPage(this, mockContainerXBlockHtml);
requestStaffOnly(true);
requestCount = requests.length;
containerPage.$('.action-staff-lock').click();
edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel
expect(requests.length).toBe(requestCount);
verifyStaffOnly(true);
});
it("does not refresh when failing to set staff only", function() {
var requestCount;
renderContainerPage(this, mockContainerXBlockHtml);
containerPage.$('.lock-checkbox').click();
requestCount = requests.length;
create_sinon.respondWithError(requests);
expect(requests.length).toBe(requestCount);
verifyStaffOnly(false);
});
});
});
describe("PublishHistory", function () {
var lastPublishCss = ".wrapper-last-publish";
it('renders the last published date and user when the block is published', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": true,
"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako" });
it('renders the last published date and user when the block is published', function() {
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
"published": true, "published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako"
});
expect(containerPage.$(lastPublishCss).text()).
toContain("Last published Jul 01, 2014 at 12:45 UTC by amako");
});
it('renders never published when the block is unpublished', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": false,
"published_on": "Jul 01, 2014 at 12:45 UTC", "published_by": "amako" });
renderContainerPage(this, mockContainerXBlockHtml);
fetch({ "published": false });
expect(containerPage.$(lastPublishCss).text()).toContain("Never published");
});
it('renders correctly when the block is published without publish info', function () {
renderContainerPage(mockContainerXBlockHtml, this);
fetch({ "id": "locator-container", "published": true, "published_on": null, "published_by": null});
renderContainerPage(this, mockContainerXBlockHtml);
fetch({
"published": true, "published_on": null, "published_by": null
});
expect(containerPage.$(lastPublishCss).text()).toContain("Previously published");
});
});
describe("Message Area", function() {
var messageSelector = '.container-message .warning',
warningMessage = 'This content is live for students. Edit with caution.';
it('is empty for a unit that is not currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
expect(containerPage.$(messageSelector).text().trim()).toBe('');
});
it('shows a message for a unit that is currently visible to students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
it('hides the message when the unit is hidden from students', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: true
});
fetch({ currently_visible_to_students: false });
expect(containerPage.$(messageSelector).text().trim()).toBe('');
});
it('shows a message when a unit is made visible', function() {
renderContainerPage(this, mockContainerXBlockHtml, {
currently_visible_to_students: false
});
fetch({ currently_visible_to_students: true });
expect(containerPage.$(messageSelector).text().trim()).toBe(warningMessage);
});
});
});
});

View File

@@ -29,6 +29,11 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
model: this.model,
view: this.view
});
this.messageView = new ContainerSubviews.MessageView({
el: this.$('.container-message'),
model: this.model
});
this.messageView.render();
this.isUnitPage = this.options.isUnitPage;
if (this.isUnitPage) {
this.xblockPublisher = new ContainerSubviews.Publisher({

View File

@@ -10,7 +10,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
* A view that calls render when "has_changes" or "published" values in XBlockInfo have changed
* after a server sync operation.
*/
var UnitStateListenerView = BaseView.extend({
var ContainerStateListenerView = BaseView.extend({
// takes XBlockInfo as a model
initialize: function() {
@@ -18,18 +18,43 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, ['has_changes', 'published'])) {
if (this.shouldRefresh(model)) {
this.render();
}
},
shouldRefresh: function(model) {
return false;
},
render: function() {}
});
var MessageView = ContainerStateListenerView.extend({
initialize: function () {
ContainerStateListenerView.prototype.initialize.call(this);
this.template = this.loadTemplate('container-message');
},
shouldRefresh: function(model) {
return ViewUtils.hasChangedAttributes(model, ['currently_visible_to_students']);
},
render: function() {
this.$el.html(this.template({
currentlyVisibleToStudents: this.model.get('currently_visible_to_students')
}));
return this;
}
});
/**
* A controller for updating the "View Live" and "Preview" buttons.
*/
var PreviewActionController = UnitStateListenerView.extend({
var PreviewActionController = ContainerStateListenerView.extend({
shouldRefresh: function(model) {
return ViewUtils.hasChangedAttributes(model, ['has_changes', 'published']);
},
render: function() {
var previewAction = this.$el.find('.button-preview'),
@@ -59,7 +84,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var Publisher = BaseView.extend({
events: {
'click .action-publish': 'publish',
'click .action-discard': 'discardChanges'
'click .action-discard': 'discardChanges',
'click .action-staff-lock': 'toggleStaffLock'
},
// takes XBlockInfo as a model
@@ -72,22 +98,25 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, ['has_changes', 'published', 'edited_on', 'edited_by'])) {
if (ViewUtils.hasChangedAttributes(model, [
'has_changes', 'published', 'edited_on', 'edited_by', 'visible_to_staff_only'
])) {
this.render();
}
},
render: function () {
this.$el.html(this.template({
has_changes: this.model.get('has_changes'),
hasChanges: this.model.get('has_changes'),
published: this.model.get('published'),
edited_on: this.model.get('edited_on'),
edited_by: this.model.get('edited_by'),
published_on: this.model.get('published_on'),
published_by: this.model.get('published_by'),
released_to_students: this.model.get('released_to_students'),
release_date: this.model.get('release_date'),
release_date_from: this.model.get('release_date_from')
editedOn: this.model.get('edited_on'),
editedBy: this.model.get('edited_by'),
publishedOn: this.model.get('published_on'),
publishedBy: this.model.get('published_by'),
releasedToStudents: this.model.get('released_to_students'),
releaseDate: this.model.get('release_date'),
releaseDateFrom: this.model.get('release_date_from'),
visibleToStaffOnly: this.model.get('visible_to_staff_only')
}));
return this;
@@ -127,10 +156,60 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
});
}
);
},
toggleStaffLock: function (e) {
var xblockInfo = this.model, self=this, enableStaffLock,
saveAndPublishStaffLock, revertCheckBox;
if (e && e.preventDefault) {
e.preventDefault();
}
enableStaffLock = !xblockInfo.get('visible_to_staff_only');
revertCheckBox = function() {
self.checkStaffLock(!enableStaffLock);
};
saveAndPublishStaffLock = function() {
return xblockInfo.save({
publish: 'republish',
metadata: {visible_to_staff_only: enableStaffLock}},
{patch: true}
).always(function() {
xblockInfo.set("publish", null);
}).done(function () {
xblockInfo.fetch();
}).fail(function() {
revertCheckBox();
});
};
this.checkStaffLock(enableStaffLock);
if (enableStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Setting Staff Lock…'),
_.bind(saveAndPublishStaffLock, self));
} else {
ViewUtils.confirmThenRunOperation(gettext("Remove Staff Lock"),
gettext("Are you sure you want to remove the staff lock? Once you publish this unit, it will be released to students on the release date."),
gettext("Remove Staff Lock"),
function() {
ViewUtils.runOperationShowingMessage(gettext('Removing Staff Lock…'),
_.bind(saveAndPublishStaffLock, self));
},
function() {
// On cancel, revert the check in the check box
revertCheckBox();
}
);
}
},
checkStaffLock: function(check) {
this.$('.action-staff-lock i').removeClass('icon-check icon-check-empty');
this.$('.action-staff-lock i').addClass(check ? 'icon-check' : 'icon-check-empty');
}
});
/**
* PublishHistory displays when and by whom the xblock was last published, if it ever was.
*/
@@ -161,6 +240,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
});
return {
'MessageView': MessageView,
'PreviewActionController': PreviewActionController,
'Publisher': Publisher,
'PublishHistory': PublishHistory

View File

@@ -33,7 +33,7 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
/**
* Confirms with the user whether to run an operation or not, and then runs it if desired.
*/
confirmThenRunOperation = function(title, message, actionLabel, operation) {
confirmThenRunOperation = function(title, message, actionLabel, operation, onCancelCallback) {
return new PromptView.Warning({
title: title,
message: message,
@@ -48,6 +48,9 @@ define(["jquery", "underscore", "gettext", "js/views/feedback_notification", "js
secondary: {
text: gettext('Cancel'),
click: function(prompt) {
if (onCancelCallback) {
onCancelCallback();
}
return prompt.hide();
}
}