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

@@ -231,7 +231,7 @@ class XBlockVisibilityTestCase(TestCase):
vertical.start = self.future
modulestore().update_item(vertical, self.dummy_user)
self.assertTrue(utils.is_xblock_visible_to_students(vertical))
self.assertTrue(utils.is_currently_visible_to_students(vertical))
def _test_visible_to_students(self, expected_visible_without_lock, name, start_date, publish=False):
"""
@@ -239,13 +239,13 @@ class XBlockVisibilityTestCase(TestCase):
with and without visible_to_staff_only set.
"""
no_staff_lock = self._create_xblock_with_start_date(name, start_date, publish, visible_to_staff_only=False)
self.assertEqual(expected_visible_without_lock, utils.is_xblock_visible_to_students(no_staff_lock))
self.assertEqual(expected_visible_without_lock, utils.is_currently_visible_to_students(no_staff_lock))
# any xblock with visible_to_staff_only set to True should not be visible to students.
staff_lock = self._create_xblock_with_start_date(
name + "_locked", start_date, publish, visible_to_staff_only=True
)
self.assertFalse(utils.is_xblock_visible_to_students(staff_lock))
self.assertFalse(utils.is_currently_visible_to_students(staff_lock))
def _create_xblock_with_start_date(self, name, start_date, publish=False, visible_to_staff_only=False):
"""Helper to create an xblock with a start date, optionally publishing it"""

View File

@@ -164,9 +164,10 @@ def compute_publish_state(xblock):
return modulestore().compute_publish_state(xblock)
def is_xblock_visible_to_students(xblock):
def is_currently_visible_to_students(xblock):
"""
Returns true if there is a published version of the xblock that has been released.
Returns true if there is a published version of the xblock that is currently visible to students.
This means that it has a release date in the past, and the xblock has not been set to staff only.
"""
try:

View File

@@ -21,7 +21,7 @@ from xblock.fields import Scope
from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist
from contentstore.utils import get_lms_link_for_item, compute_publish_state, is_xblock_visible_to_students
from contentstore.utils import get_lms_link_for_item, compute_publish_state
from contentstore.views.helpers import get_parent_xblock, is_unit, xblock_type_display_name
from contentstore.views.item import create_xblock_info
@@ -207,7 +207,6 @@ def container_handler(request, usage_key_string):
'xblock_locator': xblock.location,
'unit': unit,
'is_unit_page': is_unit_page,
'is_visible_to_students': is_xblock_visible_to_students(xblock),
'subsection': subsection,
'section': section,
'new_unit_category': 'vertical',

View File

@@ -37,6 +37,7 @@ from util.date_utils import get_default_time_display
from util.json_request import expect_json, JsonResponse
from .access import has_course_access
from contentstore.utils import is_currently_visible_to_students
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \
xblock_type_display_name, get_parent_xblock
from contentstore.views.preview import get_preview_fragment
@@ -104,10 +105,12 @@ def xblock_handler(request, usage_key_string):
to None! Absent ones will be left alone.
:nullout: which metadata fields to set to None
:graderType: change how this unit is graded
:publish: can be either -- 'make_public' (which publishes the content) or 'discard_changes'
(which reverts to the last published version). If 'discard_changes', the other fields
will not be used; that is, it is not possible to update and discard changes
in a single operation.
:publish: can be:
'make_public': publish the content
'republish': publish this item *only* if it was previously published
'discard_changes' - reverts to the last published version
Note: If 'discard_changes', the other fields will not be used; that is, it is not possible
to update and discard changes in a single operation.
The JSON representation on the updated xblock (minus children) is returned.
if usage_key_string is not specified, create a new xblock instance, either by duplicating
@@ -136,7 +139,7 @@ def xblock_handler(request, usage_key_string):
# right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key))
# TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(usage_key, request.user)
rsp = _get_module_info(_get_xblock(usage_key, request.user))
return JsonResponse(rsp)
else:
return HttpResponse(status=406)
@@ -145,9 +148,9 @@ def xblock_handler(request, usage_key_string):
_delete_item(usage_key, request.user)
return JsonResponse()
else: # Since we have a usage_key, we are updating an existing xblock.
return _save_item(
return _save_xblock(
request.user,
usage_key,
_get_xblock(usage_key, request.user),
data=request.json.get('data'),
children=request.json.get('children'),
metadata=request.json.get('metadata'),
@@ -289,8 +292,8 @@ def xblock_outline_handler(request, usage_key_string):
return Http404
def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout=None,
grader_type=None, publish=None):
def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=None,
grader_type=None, publish=None):
"""
Saves xblock w/ its fields. Has special processing for grader_type, publish, and nullout and Nones in metadata.
nullout means to truly set the field to None whereas nones in metadata mean to unset them (so they revert
@@ -298,32 +301,19 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
"""
store = modulestore()
try:
existing_item = store.get_item(usage_key)
except ItemNotFoundError:
if usage_key.category in CREATE_IF_NOT_FOUND:
# New module at this location, for pages that are not pre-created.
# Used for course info handouts.
existing_item = store.create_item(user.id, usage_key.course_key, usage_key.block_type, usage_key.block_id)
else:
raise
except InvalidLocationError:
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404)
# Don't allow updating an xblock and discarding changes in a single operation (unsupported by UI).
if publish == "discard_changes":
store.revert_to_published(usage_key, user.id)
store.revert_to_published(xblock.location, user.id)
# Returning the same sort of result that we do for other save operations. In the future,
# we may want to return the full XBlockInfo.
return JsonResponse({'id': unicode(usage_key)})
return JsonResponse({'id': unicode(xblock.location)})
old_metadata = own_metadata(existing_item)
old_content = existing_item.get_explicitly_set_fields_by_scope(Scope.content)
old_metadata = own_metadata(xblock)
old_content = xblock.get_explicitly_set_fields_by_scope(Scope.content)
if data:
# TODO Allow any scope.content fields not just "data" (exactly like the get below this)
existing_item.data = data
xblock.data = data
else:
data = old_content['data'] if 'data' in old_content else None
@@ -332,7 +322,7 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
for child in children:
child_usage_key = usage_key_with_run(child)
children_usage_keys.append(child_usage_key)
existing_item.children = children_usage_keys
xblock.children = children_usage_keys
# also commit any metadata which might have been passed along
if nullout is not None or metadata is not None:
@@ -341,53 +331,61 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
# 'apply' the submitted metadata, so we don't end up deleting system metadata.
if nullout is not None:
for metadata_key in nullout:
setattr(existing_item, metadata_key, None)
setattr(xblock, metadata_key, None)
# update existing metadata with submitted metadata (which can be partial)
# IMPORTANT NOTE: if the client passed 'null' (None) for a piece of metadata that means 'remove it'. If
# the intent is to make it None, use the nullout field
if metadata is not None:
for metadata_key, value in metadata.items():
field = existing_item.fields[metadata_key]
field = xblock.fields[metadata_key]
if value is None:
field.delete_from(existing_item)
field.delete_from(xblock)
else:
try:
value = field.from_json(value)
except ValueError:
return JsonResponse({"error": "Invalid data"}, 400)
field.write_to(existing_item, value)
field.write_to(xblock, value)
if callable(getattr(existing_item, "editor_saved", None)):
existing_item.editor_saved(user, old_metadata, old_content)
if callable(getattr(xblock, "editor_saved", None)):
xblock.editor_saved(user, old_metadata, old_content)
# commit to datastore
store.update_item(existing_item, user.id)
store.update_item(xblock, user.id)
# for static tabs, their containing course also records their display name
if usage_key.category == 'static_tab':
course = store.get_course(usage_key.course_key)
if xblock.location.category == 'static_tab':
course = store.get_course(xblock.location.course_key)
# find the course's reference to this tab and update the name.
static_tab = CourseTabList.get_tab_by_slug(course.tabs, usage_key.name)
static_tab = CourseTabList.get_tab_by_slug(course.tabs, xblock.location.name)
# only update if changed
if static_tab and static_tab['name'] != existing_item.display_name:
static_tab['name'] = existing_item.display_name
if static_tab and static_tab['name'] != xblock.display_name:
static_tab['name'] = xblock.display_name
store.update_item(course, user.id)
result = {
'id': unicode(usage_key),
'id': unicode(xblock.location),
'data': data,
'metadata': own_metadata(existing_item)
'metadata': own_metadata(xblock)
}
if grader_type is not None:
result.update(CourseGradingModel.update_section_grader_type(existing_item, grader_type, user))
result.update(CourseGradingModel.update_section_grader_type(xblock, grader_type, user))
# If publish is set to 'republish' and this item has previously been published, then this
# new item should be republished. This is used by staff locking to ensure that changing the draft
# value of the staff lock will also update the published version.
if publish == 'republish':
published = modulestore().has_item(xblock.location, revision=ModuleStoreEnum.RevisionOption.published_only)
if published:
publish = 'make_public'
# Make public after updating the xblock, in case the caller asked for both an update and a publish.
# Although not supported in the UI, Bok Choy tests use this.
# Used by Bok Choy tests and by republishing of staff locks.
if publish == 'make_public':
modulestore().publish(existing_item.location, user.id)
modulestore().publish(xblock.location, user.id)
# Note that children aren't being returned until we have a use case.
return JsonResponse(result)
@@ -552,31 +550,40 @@ def orphan_handler(request, course_key_string):
raise PermissionDenied()
def _get_module_info(usage_key, user, rewrite_static_links=True):
def _get_xblock(usage_key, user):
"""
Returns the xblock for the specified usage key. Note: if failing to find a key with a category
in the CREATE_IF_NOT_FOUND list, an xblock will be created and saved automatically.
"""
store = modulestore()
try:
return store.get_item(usage_key)
except ItemNotFoundError:
if usage_key.category in CREATE_IF_NOT_FOUND:
# Create a new one for certain categories only. Used for course info handouts.
return store.create_item(user.id, usage_key.course_key, usage_key.block_type, block_id=usage_key.block_id)
else:
raise
except InvalidLocationError:
log.error("Can't find item by location.")
return JsonResponse({"error": "Can't find item by location: " + unicode(usage_key)}, 404)
def _get_module_info(xblock, rewrite_static_links=True):
"""
metadata, data, id representation of a leaf module fetcher.
:param usage_key: A UsageKey
"""
store = modulestore()
try:
module = store.get_item(usage_key)
except ItemNotFoundError:
if usage_key.category in CREATE_IF_NOT_FOUND:
# Create a new one for certain categories only. Used for course info handouts.
module = store.create_item(user.id, usage_key.course_key, usage_key.block_type, block_id=usage_key.block_id)
else:
raise
data = getattr(module, 'data', '')
data = getattr(xblock, 'data', '')
if rewrite_static_links:
data = replace_static_urls(
data,
None,
course_id=module.location.course_key
course_id=xblock.location.course_key
)
# Note that children aren't being returned until we have a use case.
return create_xblock_info(module, data=data, metadata=own_metadata(module), include_ancestor_info=True)
return create_xblock_info(xblock, data=data, metadata=own_metadata(xblock), include_ancestor_info=True)
def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=False, include_child_info=False,
@@ -630,6 +637,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
"released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date,
"release_date_from": _get_release_date_from(xblock) if release_date else None,
"visible_to_staff_only": xblock.visible_to_staff_only,
"currently_visible_to_students": is_currently_visible_to_students(xblock),
}
if data is not None:
xblock_info["data"] = data

View File

@@ -157,39 +157,3 @@ class ContainerPageTestCase(StudioPageTestCase):
"""
empty_child_container = self._create_item(self.vertical.location, 'split_test', 'Split Test')
self.validate_preview_html(empty_child_container, self.reorderable_child_view, can_add=False)
def test_unreleased_private_container_messages(self):
"""
Verify that an unreleased private container does not display messages.
"""
self.validate_html_for_messages(self.unreleased_private_vertical, False)
def test_unreleased_public_container_messages(self):
"""
Verify that an unreleased public container does not display messages.
"""
self.validate_html_for_messages(self.unreleased_public_vertical, False)
def test_released_private_container_message(self):
"""
Verify that a released private container does not display messages.
"""
self.validate_html_for_messages(self.released_private_vertical, False)
def test_released_public_container_message(self):
"""
Verify that a released public container does display messages.
"""
self.validate_html_for_messages(self.released_public_vertical, True)
def validate_html_for_messages(self, xblock, has_messages):
"""
Validate that the specified HTML has the appropriate messages for the current student visibility state.
"""
# Verify that there are no warning messages for blocks that are not visible to students
html = self.get_page_html(xblock)
messages_html = '<div class="container-message wrapper-message">'
if has_messages:
self.assertIn(messages_html, html)
else:
self.assertNotIn(messages_html, html)

View File

@@ -567,6 +567,55 @@ class TestEditItem(ItemTest):
published = self.verify_publish_state(self.problem_usage_key, PublishState.public)
self.assertIsNone(published.due)
def test_republish(self):
""" Test republishing an item. """
new_display_name = 'New Display Name'
republish_data = {
'publish': 'republish',
'display_name': new_display_name
}
# When the problem is first created, it is only in draft (because of its category).
self.verify_publish_state(self.problem_usage_key, PublishState.private)
# Republishing when only in draft will update the draft but not cause a public item to be created.
self.client.ajax_post(
self.problem_update_url,
data={
'publish': 'republish',
'metadata': {
'display_name': new_display_name
}
}
)
self.verify_publish_state(self.problem_usage_key, PublishState.private)
draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True)
self.assertEqual(draft.display_name, new_display_name)
# Publish the item
self.client.ajax_post(
self.problem_update_url,
data={'publish': 'make_public'}
)
# Now republishing should update the published version
new_display_name_2 = 'New Display Name 2'
self.client.ajax_post(
self.problem_update_url,
data={
'publish': 'republish',
'metadata': {
'display_name': new_display_name_2
}
}
)
self.verify_publish_state(self.problem_usage_key, PublishState.public)
published = modulestore().get_item(
self.problem_usage_key,
revision=ModuleStoreEnum.RevisionOption.published_only
)
self.assertEqual(published.display_name, new_display_name_2)
def _make_draft_content_different_from_published(self):
"""
Helper method to create different draft and published versions of a problem.

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&hellip;'),
_.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&hellip;'),
_.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();
}
}

View File

@@ -11,8 +11,7 @@
//.wrapper-xblock-header {
.view-outline,
.view-container {
.view-outline {
.add-xblock-component {
text-align: center;

View File

@@ -111,11 +111,15 @@
&.staff-only,
&.is-staff-only {
@extend %bar-module-black;
&.is-scheduled .wrapper-release .copy {
text-decoration: line-through;
}
}
.bar-mod-content {
border: 0;
padding: ($baseline/2) ($baseline*.75) ($baseline*.75) ($baseline*.75);
padding: ($baseline/2) ($baseline*.75) ($baseline/4) ($baseline*.75);
.title {
margin-bottom: ($baseline/10);
@@ -123,7 +127,6 @@
}
.wrapper-last-draft {
padding: ($baseline*.75) ($baseline*.75) ($baseline/4) ($baseline*.75);
.date,
.user {
@@ -145,9 +148,9 @@
font-weight: 600;
}
.action-inline [class^="icon-"] {
margin: 0 ($baseline/4);
[class^="icon-"] {
margin-left: ($baseline/4);
color: $gray-d1;
}
}
@@ -215,107 +218,7 @@
}
.wrapper-unit-tree-location {
.draggable-drop-indicator {
display: none;
}
// need to explicitly set this since the html structure is different than the others
.section-name:hover {
background: $blue-l5;
color: $blue;
}
.subsection,
.courseware-unit {
margin: ($baseline/4) 0 0 ($baseline*.75);
}
.courseware-unit .section-item {
background-color: transparent;
}
.section-item {
@include transition(background $tmg-avg ease-in-out 0);
@include box-sizing(border-box);
@extend %t-copy-sub2;
width: 100%;
display: inline-block;
vertical-align: top;
overflow: hidden;
padding: 6px 8px 8px 16px;
background: $gray-l5;
white-space: nowrap;
text-overflow: ellipsis;
color: $gray;
&:hover {
background: $blue-l5;
color: $blue;
}
&.editing {
background-color: $orange-l3;
}
// TODO: update these once we have different pub states
.public-item {
color: $black;
}
.private-item {
color: $gray-l1;
}
.draft-item {
color: $yellow-d1;
}
.public-item:hover,
.private-item:hover,
.draft-item:hover {
color: $blue;
}
.draft-item:after,
.public-item:after,
.private-item:after {
@include font-size(9);
margin-left: 3px;
font-weight: 600;
text-transform: uppercase;
}
.draft-item:after {
content: "- draft";
}
.private-item:after {
content: "- private";
}
}
.subsection > .section-item:hover {
background-color: $gray-l5;
color: inherit;
}
.new-unit-item {
@extend %ui-btn-flat-outline;
@extend %t-action4;
width: 90%;
margin: 0 0 ($baseline/2) ($baseline/4);
border: 1px solid transparent;
padding: ($baseline/4) ($baseline/2);
font-weight: normal;
color: $gray-l2;
text-align: left;
&:hover {
box-shadow: none;
background-image: none;
}
}
// tree location-specific styles should go here
}
}
}

View File

@@ -25,7 +25,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
"editor-mode-button", "upload-dialog", "image-modal",
"add-xblock-component", "add-xblock-component-button", "add-xblock-component-menu",
"add-xblock-component-menu-problem", "xblock-string-field-editor", "publish-xblock", "publish-history",
"unit-outline"]
"unit-outline", "container-message"]
%>
<%block name="header_extras">
% for template_name in templates:
@@ -116,16 +116,7 @@ templates = ["basic-modal", "modal-button", "edit-xblock-modal",
<section class="content-area">
<article class="content-primary">
% if is_visible_to_students:
<div class="container-message wrapper-message">
<div class="message has-warnings">
<p class="warning">
<i class="icon-warning-sign"></i>
${_("This content is live for students. Edit with caution.")}
</p>
</div>
</div>
% endif
<div class="container-message wrapper-message"></div>
<section class="wrapper-xblock level-page is-hidden studio-xblock-wrapper" data-locator="${xblock_locator}" data-course-key="${xblock_locator.course_key}">
</section>
<div class="ui-loading">

View File

@@ -0,0 +1,8 @@
<% if (currentlyVisibleToStudents) { %>
<div class="message has-warnings">
<p class="warning">
<i class="icon-warning-sign"></i>
<%= gettext("This content is live for students. Edit with caution.") %>
</p>
</div>
<% } %>

View File

@@ -14,8 +14,8 @@
<% if (xblockInfo.get('category') === 'vertical') { %>
<a href="<%= xblockInfo.get('studio_url') %>"><%= xblockInfo.get('display_name') %></a>
<% } else { %>
<span class="wrapper-xblock-field is-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>">
<span class="xblock-field-value"><%= xblockInfo.get('display_name') %></span>
<span class="wrapper-xblock-field incontext-editor is-editable" data-field="display_name" data-field-display-name="<%= gettext("Display Name") %>">
<span class="xblock-field-value incontext-editor-value"><%= xblockInfo.get('display_name') %></span>
</span>
<% } %>
</h3>

View File

@@ -43,6 +43,7 @@
<section class="content-area">
<article class="content-primary window">
<div class="container-message wrapper-message"></div>
<section class="wrapper-xblock level-page studio-xblock-wrapper" data-locator="locator-container">
</section>
<div class="ui-loading is-hidden">

View File

@@ -1,35 +1,50 @@
<div class="bit-publishing <% if (published && !has_changes) { %>published<% } else { %>draft<%} %>">
<%
var publishClasses = "";
var title = gettext("Draft (Never published)");
if (published) {
if (published && hasChanges) {
publishClasses = publishClasses + " is-draft";
title = gettext("Draft (Unpublished changes)");
} else {
publishClasses = publishClasses + " is-published";
title = gettext("Published");
}
}
if (releaseDate) {
publishClasses = publishClasses + " is-scheduled";
}
if (visibleToStaffOnly) {
publishClasses = publishClasses + " is-staff-only";
title = gettext("Unpublished (Staff only)");
}
%>
<div class="bit-publishing <%= publishClasses %>">
<h3 class="bar-mod-title pub-status"><span class="sr"><%= gettext("Publishing Status") %></span>
<% if (published && !has_changes) { %>
<%= gettext("Published") %>
<% } else { %>
<%= gettext("Draft (Unpublished changes)") %>
<% } %>
<%= title %>
</h3>
<div class="wrapper-last-draft bar-mod-content">
<p class="copy meta">
<% if (has_changes && edited_on && edited_by) {
<% if (hasChanges && editedOn && editedBy) {
var message = gettext("Draft saved on %(last_saved_date)s by %(edit_username)s") %>
<%= interpolate(message, {
last_saved_date: '<span class="date">' + edited_on + '</span>',
edit_username: '<span class="user">' + edited_by + '</span>' }, true) %>
<% } else if (published_on && published_by) {
last_saved_date: '<span class="date">' + editedOn + '</span>',
edit_username: '<span class="user">' + editedBy + '</span>' }, true) %>
<% } else if (publishedOn && publishedBy) {
var message = gettext("Last published %(last_published_date)s by %(publish_username)s"); %>
<%= interpolate(message, {
last_published_date: '<span class="date">' + published_on + '</span>',
publish_username: '<span class="user">' + published_by + '</span>' }, true) %>
last_published_date: '<span class="date">' + publishedOn + '</span>',
publish_username: '<span class="user">' + publishedBy + '</span>' }, true) %>
<% } else { %>
<%= gettext("Previously published") %>
<% } %>
</p>
</div>
<!--TODO this needs strikeout styles once staff lock exists-->
<div class="wrapper-release bar-mod-content">
<h5 class="title">
<% if (published && release_date) {
if (released_to_students) { %>
<% if (published && releaseDate) {
if (releasedToStudents) { %>
<%= gettext("Released:") %>
<% } else { %>
<%= gettext("Scheduled:") %>
@@ -39,37 +54,45 @@
<% } %>
</h5>
<p class="copy">
<% if (release_date) { %>
<% if (releaseDate) { %>
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
<%= interpolate(message, {
release_date: '<span class="release-date">' + release_date + '</span>',
section_or_subsection: '<span class="release-with">' + release_date_from + '</span>' }, true) %>
release_date: '<span class="release-date">' + releaseDate + '</span>',
section_or_subsection: '<span class="release-with">' + releaseDateFrom + '</span>' }, true) %>
<% } else { %>
<%= gettext("Unscheduled") %>
<% } %>
</p>
</div>
<!--To be added in STUD-1830-->
<!--<div class="wrapper-visibility bar-mod-content">-->
<!--<h5 class="title">Will be Visible to:</h5>-->
<!--<p class="copy">Staff and Students</p>-->
<!--<p class="action-inline">-->
<!--<a href="">-->
<!--<i class="icon-unlock is-disabled"></i> Hide from Students-->
<!--</a>-->
<!--</p>-->
<!--</div>-->
<div class="wrapper-visibility bar-mod-content">
<h5 class="title"><%= gettext("Will Be Visible To:") %></h5>
<% if (visibleToStaffOnly) { %>
<p class="copy"><%= gettext("Staff Only") %></p>
<% } else { %>
<p class="copy"><%= gettext("Staff and Students") %></p>
<% } %>
<p class="action-inline">
<a href="" class="action-staff-lock" role="button" aria-pressed="<%= visibleToStaffOnly %>">
<% if (visibleToStaffOnly) { %>
<i class="icon-check"></i>
<% } else { %>
<i class="icon-check-empty"></i>
<% } %>
<%= gettext('Hide from students') %>
</a>
</p>
</div>
<div class="wrapper-pub-actions bar-mod-actions">
<ul class="action-list">
<li class="action-item">
<a class="action-publish action-primary <% if (published && !has_changes) { %>is-disabled<% } %>"
<a class="action-publish action-primary <% if (published && !hasChanges) { %>is-disabled<% } %>"
href=""><%= gettext("Publish") %>
</a>
</li>
<li class="action-item">
<a class="action-discard action-secondary <% if (!published || !has_changes) { %>is-disabled<% } %>"
<a class="action-discard action-secondary <% if (!published || !hasChanges) { %>is-disabled<% } %>"
href=""><%= gettext("Discard Changes") %>
</a>
</li>