Support staff locking on the unit page
STUD-1873
This commit is contained in:
@@ -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"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
|
||||
//.wrapper-xblock-header {
|
||||
|
||||
.view-outline,
|
||||
.view-container {
|
||||
.view-outline {
|
||||
|
||||
.add-xblock-component {
|
||||
text-align: center;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
8
cms/templates/js/container-message.underscore
Normal file
8
cms/templates/js/container-message.underscore
Normal 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>
|
||||
<% } %>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,16 +10,24 @@ class StaffPage(PageObject):
|
||||
"""
|
||||
|
||||
url = None
|
||||
STAFF_STATUS_CSS = '#staffstatus'
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css='#staffstatus').present
|
||||
return self.q(css=self.STAFF_STATUS_CSS).present
|
||||
|
||||
@property
|
||||
def staff_status(self):
|
||||
"""
|
||||
Return the current status, either Staff view or Student view
|
||||
"""
|
||||
return self.q(css='#staffstatus').text[0]
|
||||
return self.q(css=self.STAFF_STATUS_CSS).text[0]
|
||||
|
||||
def toggle_staff_view(self):
|
||||
"""
|
||||
Toggle between staff view and student view.
|
||||
"""
|
||||
self.q(css=self.STAFF_STATUS_CSS).first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def open_staff_debug_info(self):
|
||||
"""
|
||||
|
||||
@@ -81,6 +81,33 @@ class ContainerPage(PageObject):
|
||||
"""
|
||||
return self.q(css='.pub-status').first.text[0]
|
||||
|
||||
@property
|
||||
def release_title(self):
|
||||
"""
|
||||
Returns the title before the release date in the publishing sidebar component.
|
||||
"""
|
||||
return self.q(css='.wrapper-release .title').first.text[0]
|
||||
|
||||
@property
|
||||
def release_date(self):
|
||||
"""
|
||||
Returns the release date of the unit (with ancestor inherited from), as displayed
|
||||
in the publishing sidebar component.
|
||||
"""
|
||||
return self.q(css='.wrapper-release .copy').first.text[0]
|
||||
|
||||
@property
|
||||
def currently_visible_to_students(self):
|
||||
"""
|
||||
Returns True if the unit is marked as currently visible to students
|
||||
(meaning that a warning is being displayed).
|
||||
"""
|
||||
warnings = self.q(css='.container-message .warning')
|
||||
if not warnings.is_present():
|
||||
return False
|
||||
warning_text = warnings.first.text[0]
|
||||
return warning_text == "This content is live for students. Edit with caution."
|
||||
|
||||
@property
|
||||
def publish_action(self):
|
||||
"""
|
||||
@@ -96,6 +123,22 @@ class ContainerPage(PageObject):
|
||||
self.q(css='a.button.action-primary').first.click()
|
||||
self.wait_for_ajax()
|
||||
|
||||
def toggle_staff_lock(self):
|
||||
"""
|
||||
Toggles "hide from students" which enables or disables a staff-only lock.
|
||||
|
||||
Returns True if the lock is now enabled, else False.
|
||||
"""
|
||||
class_attribute_values = self.q(css='a.action-staff-lock>i').attrs('class')
|
||||
was_locked_initially = 'icon-check' in class_attribute_values
|
||||
if not was_locked_initially:
|
||||
self.q(css='a.action-staff-lock').first.click()
|
||||
else:
|
||||
click_css(self, 'a.action-staff-lock', 0, require_notification=False)
|
||||
self.q(css='a.button.action-primary').first.click()
|
||||
self.wait_for_ajax()
|
||||
return not was_locked_initially
|
||||
|
||||
def view_published_version(self):
|
||||
"""
|
||||
Clicks "View Published Version", which will open the published version of the unit page in the LMS.
|
||||
|
||||
@@ -16,6 +16,7 @@ from ..pages.lms.progress import ProgressPage
|
||||
from ..pages.lms.dashboard import DashboardPage
|
||||
from ..pages.lms.video.video import VideoPage
|
||||
from ..pages.xblock.acid import AcidView
|
||||
from ..pages.lms.courseware import CoursewarePage
|
||||
from ..fixtures.course import CourseFixture, XBlockFixtureDesc, CourseUpdateDesc
|
||||
|
||||
|
||||
@@ -421,3 +422,87 @@ class XBlockAcidChildTest(XBlockAcidBase):
|
||||
@skip('This will fail until we fix support of children in pure XBlocks')
|
||||
def test_acid_block(self):
|
||||
super(XBlockAcidChildTest, self).test_acid_block()
|
||||
|
||||
|
||||
class VisibleToStaffOnlyTest(UniqueCourseTest):
|
||||
"""
|
||||
Tests that content with visible_to_staff_only set to True cannot be viewed by students.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(VisibleToStaffOnlyTest, self).setUp()
|
||||
|
||||
course_fix = CourseFixture(
|
||||
self.course_info['org'],
|
||||
self.course_info['number'],
|
||||
self.course_info['run'],
|
||||
self.course_info['display_name']
|
||||
)
|
||||
|
||||
course_fix.add_children(
|
||||
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Subsection With Locked Unit').add_children(
|
||||
XBlockFixtureDesc('vertical', 'Locked Unit', metadata={'visible_to_staff_only': True}).add_children(
|
||||
XBlockFixtureDesc('html', 'Html Child in locked unit', data="<html>Visible only to staff</html>"),
|
||||
),
|
||||
XBlockFixtureDesc('vertical', 'Unlocked Unit').add_children(
|
||||
XBlockFixtureDesc('html', 'Html Child in unlocked unit', data="<html>Visible only to all</html>"),
|
||||
)
|
||||
),
|
||||
XBlockFixtureDesc('sequential', 'Unlocked Subsection').add_children(
|
||||
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
||||
XBlockFixtureDesc('html', 'Html Child in visible unit', data="<html>Visible to all</html>"),
|
||||
)
|
||||
),
|
||||
XBlockFixtureDesc('sequential', 'Locked Subsection', metadata={'visible_to_staff_only': True}).add_children(
|
||||
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
||||
XBlockFixtureDesc(
|
||||
'html', 'Html Child in locked subsection', data="<html>Visible only to staff</html>"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
).install()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
|
||||
def test_visible_to_staff(self):
|
||||
"""
|
||||
Scenario: All content is visible for a user marked is_staff (different from course staff)
|
||||
Given some of the course content has been marked 'visible_to_staff_only'
|
||||
And I am logged on with an account marked 'is_staff'
|
||||
Then I can see all course content
|
||||
"""
|
||||
AutoAuthPage(self.browser, username="STAFF_TESTER", email="johndoe_staff@example.com",
|
||||
course_id=self.course_id, staff=True).visit()
|
||||
|
||||
self.courseware_page.visit()
|
||||
self.assertEqual(3, len(self.course_nav.sections['Test Section']))
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit")
|
||||
self.assertEqual(["Html Child in locked unit", "Html Child in unlocked unit"], self.course_nav.sequence_items)
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Unlocked Subsection")
|
||||
self.assertEqual(["Html Child in visible unit"], self.course_nav.sequence_items)
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Locked Subsection")
|
||||
self.assertEqual(["Html Child in locked subsection"], self.course_nav.sequence_items)
|
||||
|
||||
def test_visible_to_student(self):
|
||||
"""
|
||||
Scenario: Content marked 'visible_to_staff_only' is not visible for students in the course
|
||||
Given some of the course content has been marked 'visible_to_staff_only'
|
||||
And I am logged on with an authorized student account
|
||||
Then I can only see content without 'visible_to_staff_only' set to True
|
||||
"""
|
||||
AutoAuthPage(self.browser, username="STUDENT_TESTER", email="johndoe_student@example.com",
|
||||
course_id=self.course_id, staff=False).visit()
|
||||
|
||||
self.courseware_page.visit()
|
||||
self.assertEqual(2, len(self.course_nav.sections['Test Section']))
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Subsection With Locked Unit")
|
||||
self.assertEqual(["Html Child in unlocked unit"], self.course_nav.sequence_items)
|
||||
|
||||
self.course_nav.go_to_section("Test Section", "Unlocked Subsection")
|
||||
self.assertEqual(["Html Child in visible unit"], self.course_nav.sequence_items)
|
||||
|
||||
@@ -11,9 +11,12 @@ from ..fixtures.course import XBlockFixtureDesc
|
||||
from ..pages.studio.component_editor import ComponentEditorView
|
||||
from ..pages.studio.utils import add_discussion
|
||||
from ..pages.lms.courseware import CoursewarePage
|
||||
from ..pages.lms.staff_view import StaffPage
|
||||
|
||||
from unittest import skip
|
||||
from acceptance.tests.base_studio_test import StudioCourseTest
|
||||
import datetime
|
||||
from bok_choy.promise import Promise
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@@ -46,15 +49,15 @@ class ContainerBase(StudioCourseTest):
|
||||
container = unit.xblocks[1].go_to_container()
|
||||
return container
|
||||
|
||||
def go_to_unit_page(self):
|
||||
def go_to_unit_page(self, section_name='Test Section', subsection_name='Test Subsection', unit_name='Test Unit'):
|
||||
"""
|
||||
Go to the test unit page.
|
||||
|
||||
If make_draft is true, the unit page will be put into draft mode.
|
||||
"""
|
||||
self.outline.visit()
|
||||
subsection = self.outline.section('Test Section').subsection('Test Subsection')
|
||||
return subsection.toggle_expand().unit('Test Unit').go_to()
|
||||
subsection = self.outline.section(section_name).subsection(subsection_name)
|
||||
return subsection.toggle_expand().unit(unit_name).go_to()
|
||||
|
||||
def verify_ordering(self, container, expected_orderings):
|
||||
"""
|
||||
@@ -379,6 +382,8 @@ class UnitPublishingTest(ContainerBase):
|
||||
|
||||
PUBLISHED_STATUS = "Publishing Status\nPublished"
|
||||
DRAFT_STATUS = "Publishing Status\nDraft (Unpublished changes)"
|
||||
LOCKED_STATUS = "Publishing Status\nUnpublished (Staff only)"
|
||||
RELEASE_TITLE_RELEASED = "RELEASED:"
|
||||
|
||||
def setup_fixtures(self):
|
||||
"""
|
||||
@@ -393,6 +398,8 @@ class UnitPublishingTest(ContainerBase):
|
||||
self.course_info['run'],
|
||||
self.course_info['display_name']
|
||||
)
|
||||
past_start_date = datetime.datetime(1974, 6, 22)
|
||||
self.past_start_date_text = "Jun 22, 1974 at 00:00 UTC"
|
||||
|
||||
course_fix.add_children(
|
||||
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
||||
@@ -401,6 +408,20 @@ class UnitPublishingTest(ContainerBase):
|
||||
XBlockFixtureDesc('html', 'Test html', data=self.html_content)
|
||||
)
|
||||
)
|
||||
),
|
||||
XBlockFixtureDesc('chapter', 'Unlocked Section', metadata={'start': past_start_date.isoformat()}).add_children(
|
||||
XBlockFixtureDesc('sequential', 'Unlocked Subsection').add_children(
|
||||
XBlockFixtureDesc('vertical', 'Unlocked Unit').add_children(
|
||||
XBlockFixtureDesc('problem', '<problem></problem>', data=self.html_content)
|
||||
)
|
||||
)
|
||||
),
|
||||
XBlockFixtureDesc('chapter', 'Section With Locked Unit').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Subsection With Locked Unit', metadata={'start': past_start_date.isoformat()}).add_children(
|
||||
XBlockFixtureDesc('vertical', 'Locked Unit', metadata={'visible_to_staff_only': True}).add_children(
|
||||
XBlockFixtureDesc('discussion', '', data=self.html_content)
|
||||
)
|
||||
)
|
||||
)
|
||||
).install()
|
||||
|
||||
@@ -408,61 +429,220 @@ class UnitPublishingTest(ContainerBase):
|
||||
|
||||
def test_publishing(self):
|
||||
"""
|
||||
Test the state changes when a published unit has draft changes.
|
||||
Scenario: The publish title changes based on whether or not draft content exists
|
||||
Given I have a published unit with no unpublished changes
|
||||
When I go to the unit page in Studio
|
||||
Then the title in the Publish information box is "Published"
|
||||
And the Publish button is disabled
|
||||
And when I add a component to the unit
|
||||
Then the title in the Publish information box is "Draft (Unpublished changes)"
|
||||
And the Publish button is enabled
|
||||
And when I click the Publish button
|
||||
Then the title in the Publish information box is "Published"
|
||||
"""
|
||||
unit = self.go_to_unit_page()
|
||||
self.assertEqual(self.PUBLISHED_STATUS, unit.publish_title)
|
||||
self._verify_publish_title(unit, self.PUBLISHED_STATUS)
|
||||
# Start date set in course fixture to 1970.
|
||||
self._verify_release_date_info(
|
||||
unit, self.RELEASE_TITLE_RELEASED, 'Jan 01, 1970 at 00:00 UTC with Section "Test Section"'
|
||||
)
|
||||
# Should not be able to click on Publish action -- but I don't know how to test that it is not clickable.
|
||||
# TODO: continue discussion with Muhammad and Jay about this.
|
||||
|
||||
# Add a component to the page so it will have unpublished changes.
|
||||
add_discussion(unit)
|
||||
self.assertEqual(self.DRAFT_STATUS, unit.publish_title)
|
||||
self._verify_publish_title(unit, self.DRAFT_STATUS)
|
||||
unit.publish_action.click()
|
||||
unit.wait_for_ajax()
|
||||
self.assertEqual(self.PUBLISHED_STATUS, unit.publish_title)
|
||||
self._verify_publish_title(unit, self.PUBLISHED_STATUS)
|
||||
|
||||
def test_discard_changes(self):
|
||||
"""
|
||||
Test the state after discard changes.
|
||||
Scenario: The publish title changes after "Discard Changes" is clicked
|
||||
Given I have a published unit with no unpublished changes
|
||||
When I go to the unit page in Studio
|
||||
Then the Discard Changes button is disabled
|
||||
And I add a component to the unit
|
||||
Then the title in the Publish information box is "Draft (Unpublished changes)"
|
||||
And the Discard Changes button is enabled
|
||||
And when I click the Discard Changes button
|
||||
Then the title in the Publish information box is "Published"
|
||||
"""
|
||||
unit = self.go_to_unit_page()
|
||||
add_discussion(unit)
|
||||
self.assertEqual(self.DRAFT_STATUS, unit.publish_title)
|
||||
self._verify_publish_title(unit, self.DRAFT_STATUS)
|
||||
unit.discard_changes()
|
||||
self.assertEqual(self.PUBLISHED_STATUS, unit.publish_title)
|
||||
self._verify_publish_title(unit, self.PUBLISHED_STATUS)
|
||||
|
||||
def test_view_live_no_changes(self):
|
||||
"""
|
||||
Tests viewing of live with initial published content.
|
||||
Scenario: "View Live" shows published content in LMS
|
||||
Given I have a published unit with no unpublished changes
|
||||
When I go to the unit page in Studio
|
||||
Then the View Live button is enabled
|
||||
And when I click on the View Live button
|
||||
Then I see the published content in LMS
|
||||
"""
|
||||
unit = self.go_to_unit_page()
|
||||
unit.view_published_version()
|
||||
self.assertEqual(1, self.courseware.num_xblock_components)
|
||||
self.assertEqual('html', self.courseware.xblock_component_type(0))
|
||||
self._verify_components_visible(['html'])
|
||||
|
||||
def test_view_live_changes(self):
|
||||
"""
|
||||
Tests that viewing of live with draft content does not show the draft content.
|
||||
Scenario: "View Live" does not show draft content in LMS
|
||||
Given I have a published unit with no unpublished changes
|
||||
When I go to the unit page in Studio
|
||||
And when I add a component to the unit
|
||||
And when I click on the View Live button
|
||||
Then I see the published content in LMS
|
||||
And I do not see the unpublished component
|
||||
"""
|
||||
unit = self.go_to_unit_page()
|
||||
add_discussion(unit)
|
||||
unit.view_published_version()
|
||||
self.assertEqual(1, self.courseware.num_xblock_components)
|
||||
self.assertEqual('html', self.courseware.xblock_component_type(0))
|
||||
self._verify_components_visible(['html'])
|
||||
self.assertEqual(self.html_content, self.courseware.xblock_component_html_content(0))
|
||||
|
||||
def test_view_live_after_publish(self):
|
||||
"""
|
||||
Tests viewing of live after creating draft and publishing it.
|
||||
Scenario: "View Live" shows newly published content
|
||||
Given I have a published unit with no unpublished changes
|
||||
When I go to the unit page in Studio
|
||||
And when I add a component to the unit
|
||||
And when I click the Publish button
|
||||
And when I click on the View Live button
|
||||
Then I see the newly published component
|
||||
"""
|
||||
unit = self.go_to_unit_page()
|
||||
add_discussion(unit)
|
||||
unit.publish_action.click()
|
||||
unit.view_published_version()
|
||||
self.assertEqual(2, self.courseware.num_xblock_components)
|
||||
self.assertEqual('html', self.courseware.xblock_component_type(0))
|
||||
self.assertEqual('discussion', self.courseware.xblock_component_type(1))
|
||||
self._verify_components_visible(['html', 'discussion'])
|
||||
|
||||
def test_initially_unlocked_visible_to_students(self):
|
||||
"""
|
||||
Scenario: An unlocked unit with release date in the past is visible to students
|
||||
Given I have a published unlocked unit with release date in the past
|
||||
When I go to the unit page in Studio
|
||||
Then the unit has a warning that it is visible to students
|
||||
And it is marked as "RELEASED" with release date in the past visible
|
||||
And when I click on the View Live Button
|
||||
And when I view the course as a student
|
||||
Then I see the content in the unit
|
||||
"""
|
||||
unit = self.go_to_unit_page("Unlocked Section", "Unlocked Subsection", "Unlocked Unit")
|
||||
self._verify_publish_title(unit, self.PUBLISHED_STATUS)
|
||||
self.assertTrue(unit.currently_visible_to_students)
|
||||
self._verify_release_date_info(
|
||||
unit, self.RELEASE_TITLE_RELEASED, self.past_start_date_text + ' with Section "Unlocked Section"'
|
||||
)
|
||||
unit.view_published_version()
|
||||
self._verify_student_view_visible(['problem'])
|
||||
|
||||
def test_locked_visible_to_staff_only(self):
|
||||
"""
|
||||
Scenario: After locking a unit with release date in the past, it is only visible to staff
|
||||
Given I have a published unlocked unit with release date in the past
|
||||
When I go to the unit page in Studio
|
||||
And when I select "Hide from students"
|
||||
Then the unit does not have a warning that it is visible to students
|
||||
And when I click on the View Live Button
|
||||
Then I see the content in the unit when logged in as staff
|
||||
And when I view the course as a student
|
||||
Then I do not see any content in the unit
|
||||
"""
|
||||
unit = self.go_to_unit_page("Unlocked Section", "Unlocked Subsection", "Unlocked Unit")
|
||||
checked = unit.toggle_staff_lock()
|
||||
self.assertTrue(checked)
|
||||
self.assertFalse(unit.currently_visible_to_students)
|
||||
self._verify_publish_title(unit, self.LOCKED_STATUS)
|
||||
unit.view_published_version()
|
||||
# Will initially be in staff view, locked component should be visible.
|
||||
self._verify_components_visible(['problem'])
|
||||
# Switch to student view and verify not visible
|
||||
self._verify_student_view_locked()
|
||||
|
||||
def test_initially_locked_not_visible_to_students(self):
|
||||
"""
|
||||
Scenario: A locked unit with release date in the past is not visible to students
|
||||
Given I have a published locked unit with release date in the past
|
||||
When I go to the unit page in Studio
|
||||
Then the unit does not have a warning that it is visible to students
|
||||
And it is marked as "RELEASED" with release date in the past visible
|
||||
And when I click on the View Live Button
|
||||
And when I view the course as a student
|
||||
Then I do not see any content in the unit
|
||||
"""
|
||||
unit = self.go_to_unit_page("Section With Locked Unit", "Subsection With Locked Unit", "Locked Unit")
|
||||
self._verify_publish_title(unit, self.LOCKED_STATUS)
|
||||
self.assertFalse(unit.currently_visible_to_students)
|
||||
self._verify_release_date_info(
|
||||
unit, self.RELEASE_TITLE_RELEASED,
|
||||
self.past_start_date_text + ' with Subsection "Subsection With Locked Unit"'
|
||||
)
|
||||
unit.view_published_version()
|
||||
self._verify_student_view_locked()
|
||||
|
||||
def test_unlocked_visible_to_all(self):
|
||||
"""
|
||||
Scenario: After unlocking a unit with release date in the past, it is visible to both students and staff
|
||||
Given I have a published unlocked unit with release date in the past
|
||||
When I go to the unit page in Studio
|
||||
And when I deselect "Hide from students"
|
||||
Then the unit does have a warning that it is visible to students
|
||||
And when I click on the View Live Button
|
||||
Then I see the content in the unit when logged in as staff
|
||||
And when I view the course as a student
|
||||
Then I see the content in the unit
|
||||
"""
|
||||
unit = self.go_to_unit_page("Section With Locked Unit", "Subsection With Locked Unit", "Locked Unit")
|
||||
checked = unit.toggle_staff_lock()
|
||||
self.assertFalse(checked)
|
||||
self._verify_publish_title(unit, self.PUBLISHED_STATUS)
|
||||
self.assertTrue(unit.currently_visible_to_students)
|
||||
unit.view_published_version()
|
||||
# Will initially be in staff view, components always visible.
|
||||
self._verify_components_visible(['discussion'])
|
||||
# Switch to student view and verify visible.
|
||||
self._verify_student_view_visible(['discussion'])
|
||||
|
||||
def _verify_student_view_locked(self):
|
||||
"""
|
||||
Verifies no component is visible when viewing as a student.
|
||||
"""
|
||||
StaffPage(self.browser).toggle_staff_view()
|
||||
self.assertEqual(0, self.courseware.num_xblock_components)
|
||||
|
||||
def _verify_student_view_visible(self, expected_components):
|
||||
"""
|
||||
Verifies expected components are visible when viewing as a student.
|
||||
"""
|
||||
StaffPage(self.browser).toggle_staff_view()
|
||||
self._verify_components_visible(expected_components)
|
||||
|
||||
def _verify_components_visible(self, expected_components):
|
||||
"""
|
||||
Verifies the expected components are visible (and there are no extras).
|
||||
"""
|
||||
self.assertEqual(len(expected_components), self.courseware.num_xblock_components)
|
||||
for index, component in enumerate(expected_components):
|
||||
self.assertEqual(component, self.courseware.xblock_component_type(index))
|
||||
|
||||
def _verify_release_date_info(self, unit, expected_title, expected_date):
|
||||
"""
|
||||
Verifies how the release date is displayed in the publishing sidebar.
|
||||
"""
|
||||
self.assertEqual(expected_title, unit.release_title)
|
||||
self.assertEqual(expected_date, unit.release_date)
|
||||
|
||||
def _verify_publish_title(self, unit, expected_title):
|
||||
"""
|
||||
Waits for the publish title to change to the expected value.
|
||||
"""
|
||||
def wait_for_title_change():
|
||||
return (unit.publish_title == expected_title, unit.publish_title)
|
||||
|
||||
Promise(wait_for_title_change, "Publish title incorrect. Found '" + unit.publish_title + "'").fulfill()
|
||||
|
||||
# TODO: need to work with Jay/Christine to get testing of "Preview" working.
|
||||
# def test_preview(self):
|
||||
|
||||
Reference in New Issue
Block a user