Commit 12a57817 by Ben McMorran Committed by Daniel Friedman

Add ability to set staff lock from course outline

parent 23d78fe7
......@@ -315,3 +315,111 @@ class ReleaseDateSourceTest(CourseTestCase):
"""Tests a sequential's release date being set by itself"""
self._update_release_dates(self.date_one, self.date_two, self.date_two)
self._verify_release_date_source(self.sequential, self.sequential)
class StaffLockTest(CourseTestCase):
"""Base class for testing staff lock functions."""
def setUp(self):
super(StaffLockTest, self).setUp()
self.chapter = ItemFactory.create(category='chapter', parent_location=self.course.location)
self.sequential = ItemFactory.create(category='sequential', parent_location=self.chapter.location)
self.vertical = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
self.orphan = ItemFactory.create(category='vertical', parent_location=self.sequential.location)
# Read again so that children lists are accurate
self.chapter = self.store.get_item(self.chapter.location)
self.sequential = self.store.get_item(self.sequential.location)
self.vertical = self.store.get_item(self.vertical.location)
# Orphan the orphaned xblock
self.sequential.children = [self.vertical.location]
self.sequential = self.store.update_item(self.sequential, ModuleStoreEnum.UserID.test)
def _set_staff_lock(self, xblock, is_locked):
"""If is_locked is True, xblock is staff locked. Otherwise, the xblock staff lock field is removed."""
field = xblock.fields['visible_to_staff_only']
if is_locked:
field.write_to(xblock, True)
else:
field.delete_from(xblock)
return self.store.update_item(xblock, ModuleStoreEnum.UserID.test)
def _update_staff_locks(self, chapter_locked, sequential_locked, vertical_locked):
"""
Sets the staff lock on the chapter, sequential, and vertical
If the corresponding argument is False, then the field is deleted from the xblock
"""
self.chapter = self._set_staff_lock(self.chapter, chapter_locked)
self.sequential = self._set_staff_lock(self.sequential, sequential_locked)
self.vertical = self._set_staff_lock(self.vertical, vertical_locked)
class StaffLockSourceTest(StaffLockTest):
"""Tests for finding the source of an xblock's staff lock."""
def _verify_staff_lock_source(self, item, expected_source):
"""Helper to verify that the staff lock source of a given item matches the expected source"""
source = utils.find_staff_lock_source(item)
self.assertEqual(source.location, expected_source.location)
self.assertTrue(source.visible_to_staff_only)
def test_chapter_source_for_vertical(self):
"""Tests a vertical's staff lock being set by its chapter"""
self._update_staff_locks(True, False, False)
self._verify_staff_lock_source(self.vertical, self.chapter)
def test_sequential_source_for_vertical(self):
"""Tests a vertical's staff lock being set by its sequential"""
self._update_staff_locks(True, True, False)
self._verify_staff_lock_source(self.vertical, self.sequential)
self._update_staff_locks(False, True, False)
self._verify_staff_lock_source(self.vertical, self.sequential)
def test_vertical_source_for_vertical(self):
"""Tests a vertical's staff lock being set by itself"""
self._update_staff_locks(True, True, True)
self._verify_staff_lock_source(self.vertical, self.vertical)
self._update_staff_locks(False, True, True)
self._verify_staff_lock_source(self.vertical, self.vertical)
self._update_staff_locks(False, False, True)
self._verify_staff_lock_source(self.vertical, self.vertical)
def test_orphan_has_no_source(self):
"""Tests that a orphaned xblock has no staff lock source"""
self.assertIsNone(utils.find_staff_lock_source(self.orphan))
def test_no_source_for_vertical(self):
"""Tests a vertical with no staff lock set anywhere"""
self._update_staff_locks(False, False, False)
self.assertIsNone(utils.find_staff_lock_source(self.vertical))
class InheritedStaffLockTest(StaffLockTest):
"""Tests for determining if an xblock inherits a staff lock."""
def test_no_inheritance(self):
"""Tests that a locked or unlocked vertical with no locked ancestors does not have an inherited lock"""
self._update_staff_locks(False, False, False)
self.assertFalse(utils.ancestor_has_staff_lock(self.vertical))
self._update_staff_locks(False, False, True)
self.assertFalse(utils.ancestor_has_staff_lock(self.vertical))
def test_inheritance_in_locked_section(self):
"""Tests that a locked or unlocked vertical in a locked section has an inherited lock"""
self._update_staff_locks(True, False, False)
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
self._update_staff_locks(True, False, True)
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
def test_inheritance_in_locked_subsection(self):
"""Tests that a locked or unlocked vertical in a locked subsection has an inherited lock"""
self._update_staff_locks(False, True, False)
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
self._update_staff_locks(False, True, True)
self.assertTrue(utils.ancestor_has_staff_lock(self.vertical))
def test_no_inheritance_for_orphan(self):
"""Tests that an orphaned xblock does not inherit staff lock"""
self.assertFalse(utils.ancestor_has_staff_lock(self.orphan))
......@@ -197,6 +197,44 @@ def find_release_date_source(xblock):
return find_release_date_source(parent)
def find_staff_lock_source(xblock):
"""
Returns the xblock responsible for setting this xblock's staff lock, or None if the xblock is not staff locked.
If this xblock is explicitly locked, return it, otherwise find the ancestor which sets this xblock's staff lock.
"""
# Stop searching if this xblock has explicitly set its own staff lock
if xblock.fields['visible_to_staff_only'].is_set_on(xblock):
return xblock
# Stop searching at the section level
if xblock.category == 'chapter':
return None
parent_location = modulestore().get_parent_location(xblock.location,
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
# Orphaned xblocks set their own staff lock
if not parent_location:
return None
parent = modulestore().get_item(parent_location)
return find_staff_lock_source(parent)
def ancestor_has_staff_lock(xblock, parent_xblock=None):
"""
Returns True iff one of xblock's ancestors has staff lock.
Can avoid mongo query by passing in parent_xblock.
"""
if parent_xblock is None:
parent_location = modulestore().get_parent_location(xblock.location,
revision=ModuleStoreEnum.RevisionOption.draft_preferred)
if not parent_location:
return False
parent_xblock = modulestore().get_item(parent_location)
return parent_xblock.visible_to_staff_only
def add_extra_panel_tab(tab_type, course):
"""
Used to add the panel tab to a course if it does not exist.
......
......@@ -28,17 +28,18 @@ from xmodule.modulestore import ModuleStoreEnum, PublishState
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError
from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.draft_and_published import DIRECT_ONLY_CATEGORIES
from xmodule.x_module import PREVIEW_VIEWS, STUDIO_VIEW, STUDENT_VIEW
from xmodule.course_module import DEFAULT_START_DATE
from contentstore.utils import find_release_date_source
from django.contrib.auth.models import User
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.utils import find_release_date_source, find_staff_lock_source, is_currently_visible_to_students, \
ancestor_has_staff_lock
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
......@@ -381,10 +382,10 @@ def _save_xblock(user, xblock, data=None, children=None, metadata=None, nullout=
if grader_type is not None:
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':
# If publish is set to 'republish' and this item is not in direct only categories and has previously been published,
# then this 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, but only at the unit level.
if publish == 'republish' and xblock.category not in DIRECT_ONLY_CATEGORIES:
published = modulestore().compute_publish_state(xblock) != PublishState.private
if published:
publish = 'make_public'
......@@ -653,6 +654,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
# Treat DEFAULT_START_DATE as a magic number that means the release date has not been set
release_date = get_default_time_display(xblock.start) if xblock.start != DEFAULT_START_DATE else None
visibility_state = _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None
published = modulestore().compute_publish_state(xblock) != PublishState.private
xblock_info = {
......@@ -665,7 +667,8 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
'studio_url': xblock_studio_url(xblock, parent_xblock),
"released_to_students": datetime.now(UTC) > xblock.start,
"release_date": release_date,
"visibility_state": _compute_visibility_state(xblock, child_info, is_unit_with_changes) if not xblock.category == 'course' else None,
"visibility_state": visibility_state,
"has_explicit_staff_lock": xblock.fields['visible_to_staff_only'].is_set_on(xblock),
"start": xblock.fields['start'].to_json(xblock.start),
"graded": xblock.graded,
"due_date": get_default_time_display(xblock.due),
......@@ -681,6 +684,11 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['ancestor_info'] = _create_xblock_ancestor_info(xblock, course_outline)
if child_info:
xblock_info['child_info'] = child_info
if visibility_state == VisibilityState.staff_only:
xblock_info["ancestor_has_staff_lock"] = ancestor_has_staff_lock(xblock, parent_xblock)
else:
xblock_info["ancestor_has_staff_lock"] = False
# Currently, 'edited_by', 'published_by', and 'release_date_from', and 'has_changes' are only used by the
# container page when rendering a unit. Since they are expensive to compute, only include them for units
# that are not being rendered on the course outline.
......@@ -691,6 +699,17 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
xblock_info['has_changes'] = is_unit_with_changes
if release_date:
xblock_info["release_date_from"] = _get_release_date_from(xblock)
if visibility_state == VisibilityState.staff_only:
xblock_info["staff_lock_from"] = _get_staff_lock_from(xblock)
else:
xblock_info["staff_lock_from"] = None
if course_outline:
if xblock_info["has_explicit_staff_lock"]:
xblock_info["staff_only_message"] = True
elif child_info and child_info["children"]:
xblock_info["staff_only_message"] = all([child["staff_only_message"] for child in child_info["children"]])
else:
xblock_info["staff_only_message"] = False
return xblock_info
......@@ -818,9 +837,21 @@ def _get_release_date_from(xblock):
"""
Returns a string representation of the section or subsection that sets the xblock's release date
"""
source = find_release_date_source(xblock)
# Translators: this will be a part of the release date message.
# For example, 'Released: Jul 02, 2014 at 4:00 UTC with Section "Week 1"'
return _xblock_type_and_display_name(find_release_date_source(xblock))
def _get_staff_lock_from(xblock):
"""
Returns a string representation of the section or subsection that sets the xblock's release date
"""
source = find_staff_lock_source(xblock)
return _xblock_type_and_display_name(source) if source else None
def _xblock_type_and_display_name(xblock):
"""
Returns a string representation of the xblock's type and display name
"""
return _('{section_or_subsection} "{display_name}"').format(
section_or_subsection=xblock_type_display_name(source),
display_name=source.display_name_with_default)
section_or_subsection=xblock_type_display_name(xblock),
display_name=xblock.display_name_with_default)
......@@ -102,7 +102,25 @@ function(Backbone, _, str, ModuleUtils) {
/**
* The same as `due_date` but as an ISO-formatted date string.
*/
'due': null
'due': null,
/**
* True iff this xblock is explicitly staff locked.
*/
'has_explicit_staff_lock': null,
/**
* True iff this any of this xblock's ancestors are staff locked.
*/
'ancestor_has_staff_lock': null,
/**
* The xblock which is determining the staff lock value. For instance, for a unit,
* this will either be the parent subsection or the grandparent section.
* This can be null if the xblock has no inherited staff lock.
*/
'staff_lock_from': null,
/**
* True iff this xblock should display a "Contains staff only content" message.
*/
'staff_only_message': null
},
initialize: function () {
......@@ -157,7 +175,7 @@ function(Backbone, _, str, ModuleUtils) {
* @return {Boolean}
*/
isEditableOnCourseOutline: function() {
return this.isSequential() || this.isChapter();
return this.isSequential() || this.isChapter() || this.isVertical();
}
});
return XBlockInfo;
......
define(['backbone', 'js/models/xblock_info'],
function(Backbone, XBlockInfo) {
function(Backbone, XBlockInfo) {
describe('XblockInfo isEditableOnCourseOutline', function() {
it('works correct', function() {
expect(new XBlockInfo({'category': 'chapter'}).isEditableOnCourseOutline()).toBe(true);
expect(new XBlockInfo({'category': 'course'}).isEditableOnCourseOutline()).toBe(false);
expect(new XBlockInfo({'category': 'sequential'}).isEditableOnCourseOutline()).toBe(true);
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(false);
expect(new XBlockInfo({'category': 'vertical'}).isEditableOnCourseOutline()).toBe(true);
});
});
}
......
......@@ -20,6 +20,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
display_name: 'Section',
category: 'chapter',
......@@ -38,6 +39,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
published: true,
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
has_explicit_staff_lock: false,
child_info: {
category: 'sequential',
display_name: 'Subsection',
......@@ -57,6 +59,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
edited_on: 'Jul 02, 2014 at 20:56 UTC',
edited_by: 'MockUser',
course_graders: '["Lab", "Howework"]',
has_explicit_staff_lock: false,
child_info: {
category: 'vertical',
display_name: 'Unit',
......@@ -359,18 +362,21 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($("due_date")).not.toExist();
expect($("grading_format")).not.toExist();
// Staff lock controls are always visible
expect($("#staff_lock")).toExist();
$(".edit-outline-item-modal .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-section', {
"metadata":{
"start":"2015-01-02T00:00:00.000Z",
"start":"2015-01-02T00:00:00.000Z"
}
});
expect(requests[0].requestHeaders['X-HTTP-Method-Override']).toBe('PATCH');
// This is the response for the change operation.
create_sinon.respondWithJson(requests, {});
var mockResponseSectionJSON = $.extend(true, {},
var mockResponseSectionJSON = $.extend(true, {},
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
......@@ -386,7 +392,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
])
]),
{
release_date: 'Jan 02, 2015 at 00:00 UTC',
release_date: 'Jan 02, 2015 at 00:00 UTC',
}
);
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/outline/mock-section')
......@@ -405,14 +411,15 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
return getItemHeaders('subsection').find('.wrapper-xblock-field');
};
setEditModalValues = function (start_date, due_date, grading_type) {
setEditModalValues = function (start_date, due_date, grading_type, is_locked) {
$("#start_date").val(start_date);
$("#due_date").val(due_date);
$("#grading_type").val(grading_type);
}
$("#staff_lock").prop('checked', is_locked);
};
// Contains hard-coded dates because dates are presented in different formats.
var mockServerValuesJson = $.extend(true, {},
mockServerValuesJson = $.extend(true, {},
createMockSectionJSON('mock-section', 'Mock Section', [
createMockSubsectionJSON('mock-subsection', 'Mock Subsection', [{
id: 'mock-unit',
......@@ -436,7 +443,9 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
release_date: 'Jul 09, 2014 at 00:00 UTC',
start: "2014-07-09T00:00:00Z",
format: "Lab",
due: "2014-07-10T00:00:00Z"
due: "2014-07-10T00:00:00Z",
has_explicit_staff_lock: true,
staff_only_message: true
}]
}
}
......@@ -504,11 +513,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
it('can be edited', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-subsection .configure-button').click();
setEditModalValues("7/9/2014", "7/10/2014", "Lab");
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
$(".edit-outline-item-modal .action-save").click();
create_sinon.expectJsonRequest(requests, 'POST', '/xblock/mock-subsection', {
"graderType":"Lab",
"publish": "republish",
"metadata":{
"visible_to_staff_only": true,
"start":"2014-07-09T00:00:00.000Z",
"due":"2014-07-10T00:00:00.000Z"
}
......@@ -525,18 +536,20 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-date")).toContainText("Due: Jul 10, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-value")).toContainText("Lab");
expect($(".outline-subsection .status-message-copy")).toContainText("Contains staff only content");
expect($(".outline-item .outline-subsection .status-grading-value")).toContainText("Lab");
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
expect($("#start_date").val()).toBe('7/9/2014');
expect($("#due_date").val()).toBe('7/10/2014');
expect($("#grading_type").val()).toBe('Lab');
expect($("#staff_lock").is(":checked")).toBe(true);
});
it('release date, due date and grading type can be cleared.', function() {
it('release date, due date, grading type, and staff lock can be cleared.', function() {
createCourseOutlinePage(this, mockCourseJSON, false);
outlinePage.$('.outline-item .outline-subsection .configure-button').click();
setEditModalValues("7/9/2014", "7/10/2014", "Lab");
setEditModalValues("7/9/2014", "7/10/2014", "Lab", true);
$(".edit-outline-item-modal .action-save").click();
// This is the response for the change operation.
......@@ -547,11 +560,13 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($(".outline-subsection .status-release-value")).toContainText("Jul 09, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-date")).toContainText("Due: Jul 10, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-value")).toContainText("Lab");
expect($(".outline-subsection .status-message-copy")).toContainText("Contains staff only content");
outlinePage.$('.outline-subsection .configure-button').click();
expect($("#start_date").val()).toBe('7/9/2014');
expect($("#due_date").val()).toBe('7/10/2014');
expect($("#grading_type").val()).toBe('Lab');
expect($("#staff_lock").is(":checked")).toBe(true);
$(".edit-outline-item-modal .scheduled-date-input .action-clear").click();
$(".edit-outline-item-modal .due-date-input .action-clear").click();
......@@ -559,6 +574,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($("#due_date").val()).toBe('');
$("#grading_type").val('notgraded');
$("#staff_lock").prop('checked', false);
$(".edit-outline-item-modal .action-save").click();
......@@ -573,6 +589,7 @@ define(["jquery", "js/spec_helpers/create_sinon", "js/spec_helpers/view_helpers"
expect($(".outline-subsection .status-release-value")).not.toContainText("Jul 09, 2014 at 00:00 UTC");
expect($(".outline-subsection .status-grading-date")).not.toExist();
expect($(".outline-subsection .status-grading-value")).not.toExist();
expect($(".outline-subsection .status-message-copy")).not.toContainText("Contains staff only content");
});
});
......
......@@ -146,7 +146,8 @@ define(["jquery", "underscore", "js/views/xblock_outline", "js/views/utils/view_
editXBlock: function() {
var modal = new EditSectionXBlockModal({
model: this.model,
onSave: this.refresh.bind(this)
onSave: this.refresh.bind(this),
parentInfo: this.parentInfo
});
modal.show();
......
/**
* The EditSectionXBlockModal is a Backbone view that shows an editor in a modal window.
* It has nested views: for release date, due date and grading format.
* It has nested views: for release date, due date, grading format, and staff lock.
* It is invoked using the editXBlock method and uses xblock_info as a model,
* and upon save parent invokes refresh function that fetches updated model and
* re-renders edited course outline.
*/
define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_modal',
'date', 'js/views/utils/xblock_utils', 'js/utils/date_utils'
'date', 'js/views/utils/xblock_utils', 'js/utils/date_utils', 'js/views/utils/view_utils'
],
function(
$, Backbone, _, gettext, BaseModal, date, XBlockViewUtils, DateUtils
$, Backbone, _, gettext, BaseModal, date, XBlockViewUtils, DateUtils, ViewUtils
) {
'use strict';
var EditSectionXBlockModal, BaseDateView, ReleaseDateView, DueDateView,
GradingView;
GradingView, StaffLockView;
EditSectionXBlockModal = BaseModal.extend({
events : {
......@@ -38,13 +38,10 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_mod
},
getTitle: function () {
if (this.model.isChapter() || this.model.isSequential()) {
return _.template(
gettext('<%= sectionName %> Settings'),
{sectionName: this.model.get('display_name')});
} else {
return '';
}
return _.template(
gettext('<%= sectionName %> Settings'),
{ sectionName: this.model.get('display_name') }
);
},
getContentHtml: function() {
......@@ -61,9 +58,12 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_mod
var requestData = _.extend({}, this.getRequestData(), {
metadata: this.getMetadata()
});
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
success: this.options.onSave
});
// Only update if something changed to prevent items from erroneously entering draft state
if (!_.isEqual(requestData, { metadata: {} })) {
XBlockViewUtils.updateXBlockFields(this.model, requestData, {
success: this.options.onSave
});
}
this.hide();
},
......@@ -89,7 +89,8 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_mod
*/
getContext: function () {
return _.extend({
xblockInfo: this.model
xblockInfo: this.model,
xblockType: XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo, true)
}, this.invokeComponentMethod('getContext'));
},
......@@ -115,13 +116,23 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_mod
initializeComponents: function () {
this.components = [];
this.components.push(
new ReleaseDateView({
selector: '.scheduled-date-input',
new StaffLockView({
selector: '.edit-staff-lock',
parentView: this,
model: this.model
})
);
if (this.model.isChapter() || this.model.isSequential()) {
this.components.push(
new ReleaseDateView({
selector: '.scheduled-date-input',
parentView: this,
model: this.model
})
);
}
if (this.model.isSequential()) {
this.components.push(
new DueDateView({
......@@ -240,5 +251,49 @@ define(['jquery', 'backbone', 'underscore', 'gettext', 'js/views/modals/base_mod
}
});
StaffLockView = Backbone.View.extend({
isModelLocked: function() {
return this.model.get('has_explicit_staff_lock');
},
isAncestorLocked: function() {
return this.model.get('ancestor_has_staff_lock');
},
afterRender: function () {
this.setElement(this.options.parentView.$(this.options.selector).get(0));
this.setLock(this.isModelLocked());
},
setLock: function(value) {
this.$('#staff_lock').prop('checked', value);
},
isLocked: function() {
return this.$('#staff_lock').is(':checked');
},
hasChanges: function() {
return this.isModelLocked() != this.isLocked();
},
getRequestData: function() {
return this.hasChanges() ? { publish: 'republish' } : {};
},
getMetadata: function() {
// Setting visible_to_staff_only to null when disabled will delete the field from this
// xblock, allowing it to inherit the value of its ancestors.
return this.hasChanges() ? { visible_to_staff_only: this.isLocked() ? true : null } : {};
},
getContext: function () {
return {
hasExplicitStaffLock: this.isModelLocked(),
ancestorLocked: this.isAncestorLocked()
}
}
});
return EditSectionXBlockModal;
});
......@@ -100,7 +100,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
onSync: function(model) {
if (ViewUtils.hasChangedAttributes(model, [
'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state'
'has_changes', 'published', 'edited_on', 'edited_by', 'visibility_state', 'has_explicit_staff_lock'
])) {
this.render();
}
......@@ -118,7 +118,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
publishedBy: this.model.get('published_by'),
released: this.model.get('released_to_students'),
releaseDate: this.model.get('release_date'),
releaseDateFrom: this.model.get('release_date_from')
releaseDateFrom: this.model.get('release_date_from'),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
staffLockFrom: this.model.get('staff_lock_from')
}));
return this;
......@@ -161,12 +163,13 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
},
toggleStaffLock: function (e) {
var xblockInfo = this.model, self=this, enableStaffLock,
var xblockInfo = this.model, self=this, enableStaffLock, hasInheritedStaffLock,
saveAndPublishStaffLock, revertCheckBox;
if (e && e.preventDefault) {
e.preventDefault();
}
enableStaffLock = xblockInfo.get('visibility_state') !== VisibilityState.staffOnly;
enableStaffLock = !xblockInfo.get('has_explicit_staff_lock');
hasInheritedStaffLock = xblockInfo.get('ancestor_has_staff_lock');
revertCheckBox = function() {
self.checkStaffLock(!enableStaffLock);
......@@ -189,8 +192,14 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
};
this.checkStaffLock(enableStaffLock);
if (enableStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Hiding Unit from Students&hellip;'),
if (enableStaffLock && !hasInheritedStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Hiding from Students&hellip;'),
_.bind(saveAndPublishStaffLock, self));
} else if (enableStaffLock && hasInheritedStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Explicitly Hiding from Students&hellip;'),
_.bind(saveAndPublishStaffLock, self));
} else if (!enableStaffLock && hasInheritedStaffLock) {
ViewUtils.runOperationShowingMessage(gettext('Inheriting Student Visibility&hellip;'),
_.bind(saveAndPublishStaffLock, self));
} else {
ViewUtils.confirmThenRunOperation(gettext("Make Visible to Students"),
......
......@@ -165,6 +165,18 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
return listType;
};
getXBlockType = function(category, parentInfo, translate) {
var xblockType = category;
if (category === 'chapter') {
xblockType = translate ? gettext('section') : 'section';
} else if (category === 'sequential') {
xblockType = translate ? gettext('subsection') : 'subsection';
} else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) {
xblockType = translate ? gettext('unit') : 'unit';
}
return xblockType;
};
return {
'VisibilityState': VisibilityState,
'addXBlock': addXBlock,
......@@ -172,6 +184,7 @@ define(["jquery", "underscore", "gettext", "js/views/utils/view_utils", "js/util
'updateXBlockField': updateXBlockField,
'getXBlockVisibilityClass': getXBlockVisibilityClass,
'getXBlockListTypeClass': getXBlockListTypeClass,
'updateXBlockFields': updateXBlockFields
'updateXBlockFields': updateXBlockFields,
'getXBlockType': getXBlockType
};
});
......@@ -57,9 +57,9 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var xblockInfo = this.model,
childInfo = xblockInfo.get('child_info'),
parentInfo = this.parentInfo,
xblockType = this.getXBlockType(this.model.get('category'), this.parentInfo),
xblockTypeDisplayName = this.getXBlockType(this.model.get('category'), this.parentInfo, true),
parentType = parentInfo ? this.getXBlockType(parentInfo.get('category')) : null,
xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo),
xblockTypeDisplayName = XBlockViewUtils.getXBlockType(this.model.get('category'), this.parentInfo, true),
parentType = parentInfo ? XBlockViewUtils.getXBlockType(parentInfo.get('category')) : null,
addChildName = null,
defaultNewChildName = null,
html,
......@@ -78,12 +78,14 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
xblockType: xblockType,
xblockTypeDisplayName: xblockTypeDisplayName,
parentType: parentType,
childType: childInfo ? this.getXBlockType(childInfo.category, xblockInfo) : null,
childType: childInfo ? XBlockViewUtils.getXBlockType(childInfo.category, xblockInfo) : null,
childCategory: childInfo ? childInfo.category : null,
addChildLabel: addChildName,
defaultNewChildName: defaultNewChildName,
isCollapsed: isCollapsed,
includesChildren: this.shouldRenderChildren()
includesChildren: this.shouldRenderChildren(),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
staffOnlyMessage: this.model.get('staff_only_message')
});
if (this.parentInfo) {
this.setElement($(html));
......@@ -184,18 +186,6 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
});
},
getXBlockType: function(category, parentInfo, translate) {
var xblockType = category;
if (category === 'chapter') {
xblockType = translate ? gettext('section') : 'section';
} else if (category === 'sequential') {
xblockType = translate ? gettext('subsection') : 'subsection';
} else if (category === 'vertical' && (!parentInfo || parentInfo.get('category') === 'sequential')) {
xblockType = translate ? gettext('unit') : 'unit';
}
return xblockType;
},
onSync: function(event) {
if (ViewUtils.hasChangedAttributes(this.model, ['visibility_state', 'child_info', 'display_name'])) {
this.onXBlockChange();
......@@ -266,7 +256,7 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
var self = this,
parentView = this.parentView;
event.preventDefault();
var xblockType = this.getXBlockType(this.model.get('category'), parentView.model, true);
var xblockType = XBlockViewUtils.getXBlockType(this.model.get('category'), parentView.model, true);
XBlockViewUtils.deleteXBlock(this.model, xblockType).done(function() {
if (parentView) {
parentView.onChildDeleted(self, event);
......
......@@ -59,6 +59,46 @@ textarea.text {
// forms - additional UI
form {
// CASE: cosmetic checkbox input
.checkbox-cosmetic {
.input-checkbox-checked, .input-checkbox-unchecked, .label {
display: inline-block;
vertical-align: middle;
}
.input-checkbox-checked, .input-checkbox-unchecked {
width: $baseline;
}
.input-checkbox {
@extend %cont-text-sr;
// CASE: unchecked
~ label .input-checkbox-checked {
display: none;
}
~ label .input-checkbox-unchecked {
display: inline-block;
}
// CASE: checked
&:checked {
~ label .input-checkbox-checked {
display: inline-block;
}
~ label .input-checkbox-unchecked {
display: none;
}
}
}
}
// CASE: file input
input[type=file] {
@extend %t-copy-sub1;
}
......
......@@ -54,7 +54,7 @@
// sections within a modal
.modal-section {
margin-bottom: ($baseline/2);
margin-bottom: ($baseline*0.75);
&:last-child {
margin-bottom: 0;
......@@ -245,12 +245,6 @@
margin-right: ($baseline/2);
margin-bottom: ($baseline/4);
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
label, input, textarea {
display: block;
}
label {
@extend %t-copy-sub1;
@extend %t-strong;
......@@ -288,6 +282,27 @@
.due-time {
width: ($baseline*7);
}
.tip {
@extend %t-copy-sub1;
@include transition(color, 0.15s, ease-in-out);
display: block;
margin-top: ($baseline/4);
color: $gray-l2;
}
.tip-warning {
color: $gray-d2;
}
}
// CASE: type-based input
.field-text {
// TODO: refactor the _forms.scss partial to allow for this area to inherit from it
label, input, textarea {
display: block;
}
}
// CASE: select input
......@@ -305,15 +320,52 @@
.input {
width: 100%;
}
// CASE: checkbox input
.field-checkbox {
.label, label {
margin-bottom: 0;
}
}
}
}
// UI: grading section
.edit-settings-grading {
.grading-type {
margin-bottom: $baseline;
}
}
// UI: staff lock section
.edit-staff-lock {
.checkbox-cosmetic .input-checkbox {
@extend %cont-text-sr;
// CASE: unchecked
~ .tip-warning {
display: block;
}
// CASE: checked
&:checked {
~ .tip-warning {
display: none;
}
}
}
// needed to override poorly scoped margin-bottom on any label element in a view (from _forms.scss)
.checkbox-cosmetic .label {
margin-bottom: 0;
}
}
}
// xblock custom actions
......
......@@ -157,6 +157,11 @@
.release-date {
@extend %t-strong;
}
.release-with {
@extend %t-title8;
display: block;
}
}
.wrapper-visibility {
......@@ -170,6 +175,13 @@
margin-left: ($baseline/4);
color: $gray-d1;
}
.inherited-from {
@extend %t-title8;
display: block;
}
}
.wrapper-pub-actions {
......
......@@ -6,7 +6,7 @@ var published = xblockInfo.get('published');
var statusMessage = null;
var statusType = null;
if (visibilityState === 'staff_only') {
if (staffOnlyMessage) {
statusType = 'staff-only';
statusMessage = gettext('Contains staff only content');
} else if (visibilityState === 'needs_attention') {
......
<div class="xblock-editor" data-locator="<%= xblockInfo.get('id') %>" data-course-key="<%= xblockInfo.get('courseKey') %>">
<div class="message modal-introduction">
<% if (xblockInfo.isChapter() || xblockInfo.isSequential()) { %>
<p>
<%= interpolate(gettext("Change the settings for %(display_name)s"), {display_name: xblockInfo.get('display_name')}, true) %>
</p>
<% } %>
<p>
<%= interpolate(gettext("Change the settings for %(display_name)s"), {display_name: xblockInfo.get('display_name')}, true) %>
</p>
</div>
<form class="edit-settings-form" action="#">
<div class="modal-section edit-settings-release scheduled-date-input">
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
<div class="modal-section-content has-actions">
<ul class="list-fields list-input datepair">
<li class="field field-text field-start-date field-release-date">
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
<input type="text" id="start_date" name="start_date"
value=""
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
</li>
<li class="field field-text field-start-time field-release-time">
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
<input type="text" id="start_time" name="start_time"
value=""
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
</li>
</ul>
<% if (xblockInfo.isChapter() || xblockInfo.isSequential()) { %>
<div class="modal-section edit-settings-release scheduled-date-input">
<h3 class="modal-section-title"><%= gettext('Release Date and Time') %></h3>
<div class="modal-section-content has-actions">
<ul class="list-fields list-input datepair">
<li class="field field-text field-start-date field-release-date">
<label for="start_date" class="label"><%= gettext('Release Date:') %></label>
<input type="text" id="start_date" name="start_date"
value=""
placeholder="MM/DD/YYYY" class="start-date release-date date input input-text" autocomplete="off" />
</li>
<li class="field field-text field-start-time field-release-time">
<label for="start_time" class="label"><%= gettext('Release Time in UTC:') %></label>
<input type="text" id="start_time" name="start_time"
value=""
placeholder="HH:MM" class="start-time release-time time input input-text" autocomplete="off" />
</li>
</ul>
<% if (xblockInfo.isSequential()) { %>
<ul class="list-actions">
......@@ -38,6 +37,7 @@
<% } %>
</div>
</div>
<% } %>
<% if (xblockInfo.isSequential()) { %>
<div class="modal-section edit-settings-grading">
......@@ -85,5 +85,41 @@
</div>
</div>
<% } %>
<div class="modal-section edit-staff-lock">
<h3 class="modal-section-title"><%= gettext('Student Visibility') %></h3>
<div class="modal-section-content staff-lock">
<ul class="list-fields list-input">
<li class="field field-checkbox checkbox-cosmetic">
<input type="checkbox" id="staff_lock" name="staff_lock" class="input input-checkbox" />
<label for="staff_lock" class="label">
<i class="icon-check input-checkbox-checked"></i>
<i class="icon-check-empty input-checkbox-unchecked"></i>
<%= gettext('Hide from students') %>
</label>
<% if (hasExplicitStaffLock && !ancestorLocked) { %>
<p class="tip tip-warning">
<% if (xblockInfo.isVertical()) { %>
<%= gettext('If the unit was previously published and released to students, any changes you made to the unit when it was hidden will now be visible to students.') %>
<% } else { %>
<% var message = gettext('If you make this %(xblockType)s visible to students, students will be able to see its content after the release date has passed and you have published the unit(s).'); %>
<%= interpolate(message, { xblockType: xblockType }, true) %>
<% } %>
</p>
<p class="tip tip-warning">
<% if (xblockInfo.isChapter()) { %>
<%= gettext('Any subsections or units that are explicitly hidden from students will remain hidden after you clear this option for the section.') %>
<% } %>
<% if (xblockInfo.isSequential()) { %>
<%= gettext('Any units that are explicitly hidden from students will remain hidden after you clear this option for the subsection.') %>
<% } %>
</p>
<% } %>
</li>
</ul>
</div>
</div>
</form>
</div>
......@@ -46,10 +46,11 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<h5 class="title"><%= releaseLabel %></h5>
<p class="copy">
<% if (releaseDate) { %>
<% var message = gettext("%(release_date)s with %(section_or_subsection)s") %>
<%= interpolate(message, {
release_date: '<span class="release-date">' + releaseDate + '</span>',
section_or_subsection: '<span class="release-with">' + releaseDateFrom + '</span>' }, true) %>
<span class="release-date"><%= releaseDate %></span>
<span class="release-with">
<%= interpolate(gettext('with %(release_date_from)s'), { release_date_from: releaseDateFrom }, true) %>
</span>
<% } else { %>
<%= gettext("Unscheduled") %>
<% } %>
......@@ -65,13 +66,20 @@ var visibleToStaffOnly = visibilityState === 'staff_only';
<% } %>
</h5>
<% if (visibleToStaffOnly) { %>
<p class="copy"><%= gettext("Staff Only") %></p>
<p class="copy">
<%= gettext("Staff Only") %>
<% if (!hasExplicitStaffLock) { %>
<span class="inherited-from">
<%= interpolate(gettext("with %(section_or_subsection)s"),{ section_or_subsection: staffLockFrom }, true) %>
</span>
<% } %>
</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) { %>
<a href="" class="action-staff-lock" role="button" aria-pressed="<%= hasExplicitStaffLock %>">
<% if (hasExplicitStaffLock) { %>
<i class="icon-check"></i>
<% } else { %>
<i class="icon-check-empty"></i>
......
......@@ -12,11 +12,27 @@ class CoursewarePage(CoursePage):
url_path = "courseware/"
xblock_component_selector = '.vert .xblock'
section_selector = '.chapter'
subsection_selector = '.chapter ul li'
def is_browser_on_page(self):
return self.q(css='body.courseware').present
@property
def num_sections(self):
"""
Return the number of sections in the sidebar on the page
"""
return len(self.q(css=self.section_selector))
@property
def num_subsections(self):
"""
Return the number of subsections in the sidebar on the page, including in collapsed sections
"""
return len(self.q(css=self.subsection_selector))
@property
def num_xblock_components(self):
"""
Return the number of rendered xblocks within the unit on the page
......
......@@ -133,6 +133,12 @@ class ContainerPage(PageObject):
warning_text = warnings.first.text[0]
return warning_text == "Caution: The last published version of this unit is live. By publishing changes you will change the student experience."
def shows_inherited_staff_lock(self, parent_type=None, parent_name=None):
"""
Returns True if the unit inherits staff lock from a section or subsection.
"""
return self.q(css='.bit-publishing .wrapper-visibility .copy .inherited-from').visible
@property
def publish_action(self):
"""
......@@ -153,7 +159,7 @@ class ContainerPage(PageObject):
""" Returns True if staff lock is currently enabled, False otherwise """
return 'icon-check' in self.q(css='a.action-staff-lock>i').attrs('class')
def toggle_staff_lock(self):
def toggle_staff_lock(self, inherits_staff_lock=False):
"""
Toggles "hide from students" which enables or disables a staff-only lock.
......@@ -164,7 +170,8 @@ class ContainerPage(PageObject):
self.q(css='a.action-staff-lock').first.click()
else:
click_css(self, 'a.action-staff-lock', 0, require_notification=False)
confirm_prompt(self)
if not inherits_staff_lock:
confirm_prompt(self)
self.wait_for_ajax()
return not was_locked_initially
......
......@@ -75,6 +75,16 @@ class CourseOutlineItem(object):
"""
return self.q(css=self._bounded_selector(self.STATUS_MESSAGE_SELECTOR)).text[0]
@property
def has_staff_lock_warning(self):
""" Returns True iff the 'Contains staff only content' message is visible """
return self.status_message == 'Contains staff only content' if self.has_status_message else False
@property
def is_staff_only(self):
""" Returns True if the visiblity state of this item is staff only (has a black sidebar) """
return "is-staff-only" in self.q(css=self._bounded_selector(''))[0].get_attribute("class")
def edit_name(self):
"""
Puts the item's name into editable form.
......@@ -102,6 +112,14 @@ class CourseOutlineItem(object):
self.q(css=self._bounded_selector(self.NAME_INPUT_SELECTOR)).results[0].send_keys(Keys.ENTER)
self.wait_for_ajax()
def set_staff_lock(self, is_locked):
"""
Sets the explicit staff lock of item on the container page to is_locked.
"""
modal = self.edit()
modal.is_explicitly_locked = is_locked
modal.save()
def in_editable_form(self):
"""
Return whether this outline item's display name is in its editable form.
......@@ -452,6 +470,17 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
else:
return ExpandCollapseLinkState.EXPAND
def expand_all_subsections(self):
"""
Expands all the subsections in this course.
"""
for section in self.sections():
if section.is_collapsed:
section.toggle_expand()
for subsection in section.subsections():
if subsection.is_collapsed:
subsection.toggle_expand()
class CourseOutlineModal(object):
MODAL_SELECTOR = ".edit-outline-item-modal"
......@@ -554,6 +583,28 @@ class CourseOutlineModal(object):
"Grading label is updated.",
).fulfill()
@property
def is_explicitly_locked(self):
"""
Returns true if the explict staff lock checkbox is checked, false otherwise.
"""
return self.find_css('#staff_lock')[0].is_selected()
@is_explicitly_locked.setter
def is_explicitly_locked(self, value):
"""
Checks the explicit staff lock box if value is true, otherwise unchecks the box.
"""
if value != self.is_explicitly_locked:
self.find_css('label[for="staff_lock"]').click()
EmptyPromise(lambda: value == self.is_explicitly_locked, "Explicit staff lock is updated").fulfill()
def shows_staff_lock_warning(self):
"""
Returns true iff the staff lock warning is visible.
"""
return self.find_css('.staff-lock .tip-warning').visible
def get_selected_option_text(self, element):
"""
Returns the text of the first selected option for the element.
......
......@@ -455,7 +455,7 @@ class UnitPublishingTest(ContainerBase):
self._verify_publish_title(unit, self.PUBLISHED_LIVE_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"'
unit, self.RELEASE_TITLE_RELEASED, 'Jan 01, 1970 at 00:00 UTC\nwith Section "Test Section"'
)
self._verify_last_published_and_saved(unit, self.LAST_PUBLISHED, self.LAST_PUBLISHED)
# Should not be able to click on Publish action -- but I don't know how to test that it is not clickable.
......@@ -548,7 +548,7 @@ class UnitPublishingTest(ContainerBase):
self._verify_publish_title(unit, self.PUBLISHED_LIVE_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, self.RELEASE_TITLE_RELEASED, self.past_start_date_text + '\n' + 'with Section "Unlocked Section"'
)
self._view_published_version(unit)
self._verify_student_view_visible(['problem'])
......@@ -560,6 +560,7 @@ class UnitPublishingTest(ContainerBase):
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 the unit does not display inherited staff lock
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
......@@ -569,6 +570,7 @@ class UnitPublishingTest(ContainerBase):
checked = unit.toggle_staff_lock()
self.assertTrue(checked)
self.assertFalse(unit.currently_visible_to_students)
self.assertFalse(unit.shows_inherited_staff_lock())
self._verify_publish_title(unit, self.LOCKED_STATUS)
self._view_published_version(unit)
# Will initially be in staff view, locked component should be visible.
......@@ -592,7 +594,7 @@ class UnitPublishingTest(ContainerBase):
self.assertFalse(unit.currently_visible_to_students)
self._verify_release_date_info(
unit, self.RELEASE_TITLE_RELEASE,
self.past_start_date_text + ' with Subsection "Subsection With Locked Unit"'
self.past_start_date_text + '\n' + 'with Subsection "Subsection With Locked Unit"'
)
self._view_published_version(unit)
self._verify_student_view_locked()
......@@ -620,6 +622,46 @@ class UnitPublishingTest(ContainerBase):
# Switch to student view and verify visible.
self._verify_student_view_visible(['discussion'])
def test_explicit_lock_overrides_implicit_subsection_lock_information(self):
"""
Scenario: A unit's explicit staff lock hides its inherited subsection staff lock information
Given I have a course with sections, subsections, and units
And I have enabled explicit staff lock on a subsection
When I visit the unit page
Then the unit page shows its inherited staff lock
And I enable explicit staff locking
Then the unit page does not show its inherited staff lock
And when I disable explicit staff locking
Then the unit page now shows its inherited staff lock
"""
self.outline.visit()
self.outline.expand_all_subsections()
subsection = self.outline.section_at(0).subsection_at(0)
unit = subsection.unit_at(0)
subsection.set_staff_lock(True)
unit_page = unit.go_to()
self._verify_explicit_lock_overrides_implicit_lock_information(unit_page)
def test_explicit_lock_overrides_implicit_section_lock_information(self):
"""
Scenario: A unit's explicit staff lock hides its inherited subsection staff lock information
Given I have a course with sections, subsections, and units
And I have enabled explicit staff lock on a section
When I visit the unit page
Then the unit page shows its inherited staff lock
And I enable explicit staff locking
Then the unit page does not show its inherited staff lock
And when I disable explicit staff locking
Then the unit page now shows its inherited staff lock
"""
self.outline.visit()
self.outline.expand_all_subsections()
section = self.outline.section_at(0)
unit = section.subsection_at(0).unit_at(0)
section.set_staff_lock(True)
unit_page = unit.go_to()
self._verify_explicit_lock_overrides_implicit_lock_information(unit_page)
def test_published_unit_with_draft_child(self):
"""
Scenario: A published unit with a draft child can be published
......@@ -748,6 +790,16 @@ class UnitPublishingTest(ContainerBase):
self.assertTrue(expected_published_prefix in unit.last_published_text)
self.assertTrue(expected_saved_prefix in unit.last_saved_text)
def _verify_explicit_lock_overrides_implicit_lock_information(self, unit_page):
"""
Verifies that a unit with inherited staff lock does not display inherited information when explicitly locked.
"""
self.assertTrue(unit_page.shows_inherited_staff_lock())
unit_page.toggle_staff_lock(inherits_staff_lock=True)
self.assertFalse(unit_page.shows_inherited_staff_lock())
unit_page.toggle_staff_lock(inherits_staff_lock=True)
self.assertTrue(unit_page.shows_inherited_staff_lock())
# TODO: need to work with Jay/Christine to get testing of "Preview" working.
# def test_preview(self):
# unit = self.go_to_unit_page()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment