Merge pull request #4740 from edx/dan-ben/staff-lock-outline
Add ability to set staff lock from course outline
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -20,7 +20,7 @@ from contentstore.views.component import (
|
||||
component_handler, get_component_templates
|
||||
)
|
||||
|
||||
from contentstore.views.item import create_xblock_info, ALWAYS, VisibilityState
|
||||
from contentstore.views.item import create_xblock_info, ALWAYS, VisibilityState, _xblock_type_and_display_name
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.capa_module import CapaDescriptor
|
||||
@@ -308,7 +308,8 @@ class TestCreateItem(ItemTest):
|
||||
|
||||
# Check that its name is not None
|
||||
new_tab = self.get_item_from_modulestore(usage_key)
|
||||
self.assertEquals(new_tab.display_name, 'Empty')
|
||||
self.assertEquals(new_tab.display_name, 'Empty')
|
||||
|
||||
|
||||
class TestDuplicateItem(ItemTest):
|
||||
"""
|
||||
@@ -669,6 +670,20 @@ class TestEditItem(ItemTest):
|
||||
)
|
||||
self.assertEqual(published.display_name, new_display_name_2)
|
||||
|
||||
def test_direct_only_categories_not_republished(self):
|
||||
"""Verify that republish is ignored for items in DIRECT_ONLY_CATEGORIES"""
|
||||
# Create a vertical child with published and unpublished versions.
|
||||
# If the parent sequential is not re-published, then the child problem should also not be re-published.
|
||||
resp = self.create_xblock(parent_usage_key=self.seq_usage_key, display_name='vertical', category='vertical')
|
||||
vertical_usage_key = self.response_usage_key(resp)
|
||||
vertical_update_url = reverse_usage_url('xblock_handler', vertical_usage_key)
|
||||
self.client.ajax_post(vertical_update_url, data={'publish': 'make_public'})
|
||||
self.client.ajax_post(vertical_update_url, data={'metadata': {'display_name': 'New Display Name'}})
|
||||
|
||||
self._verify_published_with_draft(self.seq_usage_key)
|
||||
self.client.ajax_post(self.seq_update_url, data={'publish': 'republish'})
|
||||
self._verify_published_with_draft(self.seq_usage_key)
|
||||
|
||||
def _make_draft_content_different_from_published(self):
|
||||
"""
|
||||
Helper method to create different draft and published versions of a problem.
|
||||
@@ -1323,10 +1338,13 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
"""
|
||||
Creates a child xblock for the given parent.
|
||||
"""
|
||||
return ItemFactory.create(
|
||||
child = ItemFactory.create(
|
||||
parent_location=parent.location, category=category, display_name=display_name,
|
||||
user_id=self.user.id, publish_item=publish_item, visible_to_staff_only=staff_only
|
||||
user_id=self.user.id, publish_item=publish_item
|
||||
)
|
||||
if staff_only:
|
||||
self._enable_staff_only(child.location)
|
||||
return child
|
||||
|
||||
def _get_child_xblock_info(self, xblock_info, index):
|
||||
"""
|
||||
@@ -1346,6 +1364,17 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
include_children_predicate=ALWAYS,
|
||||
)
|
||||
|
||||
def _get_xblock_outline_info(self, location):
|
||||
"""
|
||||
Returns the xblock info for the specified location as neeeded for the course outline page.
|
||||
"""
|
||||
return create_xblock_info(
|
||||
modulestore().get_item(location),
|
||||
include_child_info=True,
|
||||
include_children_predicate=ALWAYS,
|
||||
course_outline=True
|
||||
)
|
||||
|
||||
def _set_release_date(self, location, start):
|
||||
"""
|
||||
Sets the release date for the specified xblock.
|
||||
@@ -1354,12 +1383,12 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
xblock.start = start
|
||||
self.store.update_item(xblock, self.user.id)
|
||||
|
||||
def _set_staff_only(self, location, staff_only):
|
||||
def _enable_staff_only(self, location):
|
||||
"""
|
||||
Sets staff only for the specified xblock.
|
||||
Enables staff only for the specified xblock.
|
||||
"""
|
||||
xblock = modulestore().get_item(location)
|
||||
xblock.visible_to_staff_only = staff_only
|
||||
xblock.visible_to_staff_only = True
|
||||
self.store.update_item(xblock, self.user.id)
|
||||
|
||||
def _set_display_name(self, location, display_name):
|
||||
@@ -1370,22 +1399,50 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
xblock.display_name = display_name
|
||||
self.store.update_item(xblock, self.user.id)
|
||||
|
||||
def _verify_visibility_state(self, xblock_info, expected_state, path=None):
|
||||
def _verify_xblock_info_state(self, xblock_info, xblock_info_field, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the publish state of an item in the xblock_info. If no path is provided
|
||||
then the root item will be verified.
|
||||
Verify the state of an xblock_info field. If no path is provided then the root item will be verified.
|
||||
If should_equal is True, assert that the current state matches the expected state, otherwise assert that they
|
||||
do not match.
|
||||
"""
|
||||
if path:
|
||||
direct_child_xblock_info = self._get_child_xblock_info(xblock_info, path[0])
|
||||
remaining_path = path[1:] if len(path) > 1 else None
|
||||
self._verify_visibility_state(direct_child_xblock_info, expected_state, remaining_path)
|
||||
self._verify_xblock_info_state(direct_child_xblock_info, xblock_info_field, expected_state, remaining_path, should_equal)
|
||||
else:
|
||||
self.assertEqual(xblock_info['visibility_state'], expected_state)
|
||||
if should_equal:
|
||||
self.assertEqual(xblock_info[xblock_info_field], expected_state)
|
||||
else:
|
||||
self.assertNotEqual(xblock_info[xblock_info_field], expected_state)
|
||||
|
||||
def _verify_has_staff_only_message(self, xblock_info, expected_state, path=None):
|
||||
"""
|
||||
Verify the staff_only_message field of xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'staff_only_message', expected_state, path)
|
||||
|
||||
def _verify_visibility_state(self, xblock_info, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the publish state of an item in the xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'visibility_state', expected_state, path, should_equal)
|
||||
|
||||
def _verify_explicit_staff_lock_state(self, xblock_info, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the explicit staff lock state of an item in the xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'has_explicit_staff_lock', expected_state, path, should_equal)
|
||||
|
||||
def _verify_staff_lock_from_state(self, xblock_info, expected_state, path=None, should_equal=True):
|
||||
"""
|
||||
Verify the staff_lock_from state of an item in the xblock_info.
|
||||
"""
|
||||
self._verify_xblock_info_state(xblock_info, 'staff_lock_from', expected_state, path, should_equal)
|
||||
|
||||
def test_empty_chapter(self):
|
||||
empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter")
|
||||
xblock_info = self._get_xblock_info(empty_chapter.location)
|
||||
self.assertEqual(xblock_info['visibility_state'], VisibilityState.unscheduled)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.unscheduled)
|
||||
|
||||
def test_empty_sequential(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
@@ -1465,16 +1522,83 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
# Finally verify the state of the chapter
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.ready)
|
||||
|
||||
def test_staff_only(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
def test_staff_only_section(self):
|
||||
"""
|
||||
Tests that an explicitly staff-locked section and all of its children are visible to staff only.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter", staff_only=True)
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
unit = self._create_child(sequential, 'vertical', "Published Unit")
|
||||
self._set_staff_only(unit.location, True)
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_explicit_staff_lock_state(xblock_info, True)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_staff_lock_from_state(xblock_info, _xblock_type_and_display_name(chapter), path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_no_staff_only_section(self):
|
||||
"""
|
||||
Tests that a section with a staff-locked subsection and a visible subsection is not staff locked itself.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
self._create_child(chapter, 'sequential', "Test Visible Sequential")
|
||||
self._create_child(chapter, 'sequential', "Test Staff Locked Sequential", staff_only=True)
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[0], should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[1])
|
||||
|
||||
def test_staff_only_subsection(self):
|
||||
"""
|
||||
Tests that an explicitly staff-locked subsection and all of its children are visible to staff only.
|
||||
In this case the parent section is also visible to staff only because all of its children are staff only.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential", staff_only=True)
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, True, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_staff_lock_from_state(xblock_info, _xblock_type_and_display_name(sequential), path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_no_staff_only_subsection(self):
|
||||
"""
|
||||
Tests that a subsection with a staff-locked unit and a visible unit is not staff locked itself.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
self._create_child(sequential, 'vertical', "Locked Unit", staff_only=True)
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, self.FIRST_SUBSECTION_PATH, should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, self.FIRST_UNIT_PATH, should_equal=False)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, self.SECOND_UNIT_PATH)
|
||||
|
||||
def test_staff_only_unit(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
unit = self._create_child(sequential, 'vertical', "Unit", staff_only=True)
|
||||
xblock_info = self._get_xblock_info(chapter.location)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, False, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_explicit_staff_lock_state(xblock_info, True, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
self._verify_staff_lock_from_state(xblock_info, _xblock_type_and_display_name(unit), path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_unscheduled_section_with_live_subsection(self):
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
@@ -1499,3 +1623,27 @@ class TestXBlockPublishingInfo(ItemTest):
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.live, path=self.FIRST_UNIT_PATH)
|
||||
self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH)
|
||||
|
||||
def test_locked_section_staff_only_message(self):
|
||||
"""
|
||||
Tests that a locked section has a staff only message and its descendants do not.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter", staff_only=True)
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
self._create_child(sequential, 'vertical', "Unit")
|
||||
xblock_info = self._get_xblock_outline_info(chapter.location)
|
||||
self._verify_has_staff_only_message(xblock_info, True)
|
||||
self._verify_has_staff_only_message(xblock_info, False, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_has_staff_only_message(xblock_info, False, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
def test_locked_unit_staff_only_message(self):
|
||||
"""
|
||||
Tests that a lone locked unit has a staff only message along with its ancestors.
|
||||
"""
|
||||
chapter = self._create_child(self.course, 'chapter', "Test Chapter")
|
||||
sequential = self._create_child(chapter, 'sequential', "Test Sequential")
|
||||
self._create_child(sequential, 'vertical', "Unit", staff_only=True)
|
||||
xblock_info = self._get_xblock_outline_info(chapter.location)
|
||||
self._verify_has_staff_only_message(xblock_info, True)
|
||||
self._verify_has_staff_only_message(xblock_info, True, path=self.FIRST_SUBSECTION_PATH)
|
||||
self._verify_has_staff_only_message(xblock_info, True, path=self.FIRST_UNIT_PATH)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,6 +124,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
lastDraftCss = ".wrapper-last-draft",
|
||||
releaseDateTitleCss = ".wrapper-release .title",
|
||||
releaseDateContentCss = ".wrapper-release .copy",
|
||||
releaseDateDateCss = ".wrapper-release .copy .release-date",
|
||||
releaseDateWithCss = ".wrapper-release .copy .release-with",
|
||||
promptSpies, sendDiscardChangesToServer, verifyPublishingBitUnscheduled;
|
||||
|
||||
sendDiscardChangesToServer = function() {
|
||||
@@ -342,8 +344,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
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"');
|
||||
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
|
||||
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
|
||||
});
|
||||
|
||||
it('renders correctly when released', function () {
|
||||
@@ -353,8 +355,8 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
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"');
|
||||
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
|
||||
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
|
||||
});
|
||||
|
||||
it('renders correctly when the release date is not set', function () {
|
||||
@@ -375,20 +377,22 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
});
|
||||
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"');
|
||||
expect(containerPage.$(releaseDateDateCss).text()).toContain('Jul 02, 2014 at 14:20 UTC');
|
||||
expect(containerPage.$(releaseDateWithCss).text()).toContain('with Section "Week 1"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Content Visibility", function () {
|
||||
var requestStaffOnly, verifyStaffOnly, promptSpy,
|
||||
var requestStaffOnly, verifyStaffOnly, verifyExplicitStaffOnly, verifyImplicitStaffOnly, promptSpy,
|
||||
visibilityTitleCss = '.wrapper-visibility .title';
|
||||
|
||||
requestStaffOnly = function(isStaffOnly) {
|
||||
var newVisibilityState;
|
||||
|
||||
containerPage.$('.action-staff-lock').click();
|
||||
|
||||
// If removing the staff lock, click 'Yes' to confirm
|
||||
if (!isStaffOnly) {
|
||||
// If removing explicit staff lock with no implicit staff lock, click 'Yes' to confirm
|
||||
if (!isStaffOnly && !containerPage.model.get('ancestor_has_staff_lock')) {
|
||||
edit_helpers.confirmPrompt(promptSpy);
|
||||
}
|
||||
|
||||
@@ -403,24 +407,46 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
visible_to_staff_only: isStaffOnly ? true : null
|
||||
}
|
||||
});
|
||||
|
||||
create_sinon.expectJsonRequest(requests, 'GET', '/xblock/locator-container');
|
||||
if (isStaffOnly || containerPage.model.get('ancestor_has_staff_lock')) {
|
||||
newVisibilityState = VisibilityState.staffOnly;
|
||||
} else {
|
||||
newVisibilityState = VisibilityState.live;
|
||||
}
|
||||
create_sinon.respondWithJson(requests, createXBlockInfo({
|
||||
published: containerPage.model.get('published'),
|
||||
visibility_state: isStaffOnly ? VisibilityState.staffOnly : VisibilityState.live,
|
||||
has_explicit_staff_lock: isStaffOnly,
|
||||
visibility_state: newVisibilityState,
|
||||
release_date: "Jul 02, 2000 at 14:20 UTC"
|
||||
}));
|
||||
};
|
||||
|
||||
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.$('.wrapper-visibility .copy').text()).toContain('Staff Only');
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(staffOnlyClass);
|
||||
expect(containerPage.$(bitPublishingCss)).toHaveClass(scheduledClass);
|
||||
} else {
|
||||
expect(containerPage.$('.wrapper-visibility .copy').text().trim()).toBe('Staff and Students');
|
||||
expect(containerPage.$(bitPublishingCss)).not.toHaveClass(staffOnlyClass);
|
||||
verifyExplicitStaffOnly(false);
|
||||
verifyImplicitStaffOnly(false);
|
||||
}
|
||||
};
|
||||
|
||||
verifyExplicitStaffOnly = function(isStaffOnly) {
|
||||
if (isStaffOnly) {
|
||||
expect(containerPage.$('.action-staff-lock i')).toHaveClass('icon-check');
|
||||
} 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(staffOnlyClass);
|
||||
}
|
||||
};
|
||||
|
||||
verifyImplicitStaffOnly = function(isStaffOnly) {
|
||||
if (isStaffOnly) {
|
||||
expect(containerPage.$('.wrapper-visibility .inherited-from')).toExist();
|
||||
} else {
|
||||
expect(containerPage.$('.wrapper-visibility .inherited-from')).not.toExist();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -444,36 +470,79 @@ define(["jquery", "underscore", "underscore.string", "js/spec_helpers/create_sin
|
||||
published: true,
|
||||
has_changes: true
|
||||
});
|
||||
expect(containerPage.$(visibilityTitleCss).text()).toContain('Will Be Visible To')
|
||||
expect(containerPage.$(visibilityTitleCss).text()).toContain('Will Be Visible To');
|
||||
});
|
||||
|
||||
it("can be set to staff only", function() {
|
||||
it("can be explicitly set to staff only", function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml);
|
||||
requestStaffOnly(true);
|
||||
verifyExplicitStaffOnly(true);
|
||||
verifyImplicitStaffOnly(false);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("can remove staff only setting", function() {
|
||||
it("can be implicitly set to staff only", function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
ancestor_has_staff_lock: true,
|
||||
staff_lock_from: "Section Foo"
|
||||
});
|
||||
verifyImplicitStaffOnly(true);
|
||||
verifyExplicitStaffOnly(false);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("can be explicitly and implicitly set to staff only", function() {
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
ancestor_has_staff_lock: true,
|
||||
staff_lock_from: "Section Foo"
|
||||
});
|
||||
requestStaffOnly(true);
|
||||
// explicit staff lock overrides the display of implicit staff lock
|
||||
verifyImplicitStaffOnly(false);
|
||||
verifyExplicitStaffOnly(true);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("can remove explicit staff only setting without having implicit staff only", function() {
|
||||
promptSpy = edit_helpers.createPromptSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
release_date: "Jul 02, 2000 at 14:20 UTC"
|
||||
has_explicit_staff_lock: true,
|
||||
ancestor_has_staff_lock: false
|
||||
});
|
||||
requestStaffOnly(false);
|
||||
verifyStaffOnly(false);
|
||||
});
|
||||
|
||||
it("can remove explicit staff only setting while having implicit staff only", function() {
|
||||
promptSpy = edit_helpers.createPromptSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
ancestor_has_staff_lock: true,
|
||||
has_explicit_staff_lock: true,
|
||||
staff_lock_from: "Section Foo"
|
||||
});
|
||||
requestStaffOnly(false);
|
||||
verifyExplicitStaffOnly(false);
|
||||
verifyImplicitStaffOnly(true);
|
||||
verifyStaffOnly(true);
|
||||
});
|
||||
|
||||
it("does not refresh if removing staff only is canceled", function() {
|
||||
var requestCount;
|
||||
promptSpy = edit_helpers.createPromptSpy();
|
||||
renderContainerPage(this, mockContainerXBlockHtml, {
|
||||
visibility_state: VisibilityState.staffOnly,
|
||||
release_date: "Jul 02, 2000 at 14:20 UTC"
|
||||
has_explicit_staff_lock: true,
|
||||
ancestor_has_staff_lock: false
|
||||
});
|
||||
requestCount = requests.length;
|
||||
containerPage.$('.action-staff-lock').click();
|
||||
edit_helpers.confirmPrompt(promptSpy, true); // Click 'No' to cancel
|
||||
expect(requests.length).toBe(requestCount);
|
||||
verifyExplicitStaffOnly(true);
|
||||
verifyStaffOnly(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();
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* 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…'),
|
||||
if (enableStaffLock && !hasInheritedStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Hiding from Students…'),
|
||||
_.bind(saveAndPublishStaffLock, self));
|
||||
} else if (enableStaffLock && hasInheritedStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Explicitly Hiding from Students…'),
|
||||
_.bind(saveAndPublishStaffLock, self));
|
||||
} else if (!enableStaffLock && hasInheritedStaffLock) {
|
||||
ViewUtils.runOperationShowingMessage(gettext('Inheriting Student Visibility…'),
|
||||
_.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') {
|
||||
|
||||
@@ -1,30 +1,29 @@
|
||||
<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,10 +12,26 @@ 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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,6 +11,7 @@ from bok_choy.promise import EmptyPromise
|
||||
from ..pages.studio.overview import CourseOutlinePage, ContainerPage, ExpandCollapseLinkState
|
||||
from ..pages.studio.utils import add_discussion
|
||||
from ..pages.lms.courseware import CoursewarePage
|
||||
from ..pages.lms.staff_view import StaffPage
|
||||
from ..fixtures.course import XBlockFixtureDesc
|
||||
|
||||
from .base_studio_test import StudioCourseTest
|
||||
@@ -119,7 +120,9 @@ class WarningMessagesTest(CourseOutlineTest):
|
||||
return XBlockFixtureDesc('chapter', name).add_children(
|
||||
subsection if unit_state.publish_state == self.PublishState.NEVER_PUBLISHED
|
||||
else subsection.add_children(
|
||||
XBlockFixtureDesc('vertical', name, metadata={'visible_to_staff_only': unit_state.is_locked})
|
||||
XBlockFixtureDesc('vertical', name, metadata={
|
||||
'visible_to_staff_only': True if unit_state.is_locked else None
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@@ -403,6 +406,388 @@ class EditingSectionsTest(CourseOutlineTest):
|
||||
self.assertIn(release_text, self.course_outline_page.section_at(0).subsection_at(0).release_date)
|
||||
|
||||
|
||||
@attr('shard_2')
|
||||
class StaffLockTest(CourseOutlineTest):
|
||||
"""
|
||||
Feature: Sections, subsections, and units can be locked and unlocked from the course outline.
|
||||
"""
|
||||
|
||||
__test__ = True
|
||||
|
||||
def populate_course_fixture(self, course_fixture):
|
||||
""" Create a course with one section, two subsections, and four units """
|
||||
course_fixture.add_children(
|
||||
XBlockFixtureDesc('chapter', '1').add_children(
|
||||
XBlockFixtureDesc('sequential', '1.1').add_children(
|
||||
XBlockFixtureDesc('vertical', '1.1.1'),
|
||||
XBlockFixtureDesc('vertical', '1.1.2')
|
||||
),
|
||||
XBlockFixtureDesc('sequential', '1.2').add_children(
|
||||
XBlockFixtureDesc('vertical', '1.2.1'),
|
||||
XBlockFixtureDesc('vertical', '1.2.2')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def _verify_descendants_are_staff_only(self, item):
|
||||
"""Verifies that all the descendants of item are staff only"""
|
||||
self.assertTrue(item.is_staff_only)
|
||||
if hasattr(item, 'children'):
|
||||
for child in item.children():
|
||||
self._verify_descendants_are_staff_only(child)
|
||||
|
||||
def _remove_staff_lock_and_verify_warning(self, outline_item, expect_warning):
|
||||
"""Removes staff lock from a course outline item and checks whether or not a warning appears."""
|
||||
modal = outline_item.edit()
|
||||
modal.is_explicitly_locked = False
|
||||
if expect_warning:
|
||||
self.assertTrue(modal.shows_staff_lock_warning())
|
||||
else:
|
||||
self.assertFalse(modal.shows_staff_lock_warning())
|
||||
modal.save()
|
||||
|
||||
def _toggle_lock_on_unlocked_item(self, outline_item):
|
||||
"""Toggles outline_item's staff lock on and then off, verifying the staff lock warning"""
|
||||
self.assertFalse(outline_item.has_staff_lock_warning)
|
||||
outline_item.set_staff_lock(True)
|
||||
self.assertTrue(outline_item.has_staff_lock_warning)
|
||||
self._verify_descendants_are_staff_only(outline_item)
|
||||
outline_item.set_staff_lock(False)
|
||||
self.assertFalse(outline_item.has_staff_lock_warning)
|
||||
|
||||
def _verify_explicit_staff_lock_remains_after_unlocking_parent(self, child_item, parent_item):
|
||||
"""Verifies that child_item's explicit staff lock remains after removing parent_item's staff lock"""
|
||||
child_item.set_staff_lock(True)
|
||||
parent_item.set_staff_lock(True)
|
||||
self.assertTrue(parent_item.has_staff_lock_warning)
|
||||
self.assertTrue(child_item.has_staff_lock_warning)
|
||||
parent_item.set_staff_lock(False)
|
||||
self.assertFalse(parent_item.has_staff_lock_warning)
|
||||
self.assertTrue(child_item.has_staff_lock_warning)
|
||||
|
||||
def test_units_can_be_locked(self):
|
||||
"""
|
||||
Scenario: Units can be locked and unlocked from the course outline page
|
||||
Given I have a course with a unit
|
||||
When I click on the configuration icon
|
||||
And I enable explicit staff locking
|
||||
And I click save
|
||||
Then the unit shows a staff lock warning
|
||||
And when I click on the configuration icon
|
||||
And I disable explicit staff locking
|
||||
And I click save
|
||||
Then the unit does not show a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0)
|
||||
self._toggle_lock_on_unlocked_item(unit)
|
||||
|
||||
def test_subsections_can_be_locked(self):
|
||||
"""
|
||||
Scenario: Subsections can be locked and unlocked from the course outline page
|
||||
Given I have a course with a subsection
|
||||
When I click on the subsection's configuration icon
|
||||
And I enable explicit staff locking
|
||||
And I click save
|
||||
Then the subsection shows a staff lock warning
|
||||
And all its descendants are staff locked
|
||||
And when I click on the subsection's configuration icon
|
||||
And I disable explicit staff locking
|
||||
And I click save
|
||||
Then the the subsection does not show a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
self._toggle_lock_on_unlocked_item(subsection)
|
||||
|
||||
def test_sections_can_be_locked(self):
|
||||
"""
|
||||
Scenario: Sections can be locked and unlocked from the course outline page
|
||||
Given I have a course with a section
|
||||
When I click on the section's configuration icon
|
||||
And I enable explicit staff locking
|
||||
And I click save
|
||||
Then the section shows a staff lock warning
|
||||
And all its descendants are staff locked
|
||||
And when I click on the section's configuration icon
|
||||
And I disable explicit staff locking
|
||||
And I click save
|
||||
Then the section does not show a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
self._toggle_lock_on_unlocked_item(section)
|
||||
|
||||
def test_explicit_staff_lock_remains_after_unlocking_section(self):
|
||||
"""
|
||||
Scenario: An explicitly locked unit is still locked after removing an inherited lock from a section
|
||||
Given I have a course with sections, subsections, and units
|
||||
And I have enabled explicit staff lock on a section and one of its units
|
||||
When I click on the section's configuration icon
|
||||
And I disable explicit staff locking
|
||||
And I click save
|
||||
Then the unit still shows a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
unit = section.subsection_at(0).unit_at(0)
|
||||
self._verify_explicit_staff_lock_remains_after_unlocking_parent(unit, section)
|
||||
|
||||
def test_explicit_staff_lock_remains_after_unlocking_subsection(self):
|
||||
"""
|
||||
Scenario: An explicitly locked unit is still locked after removing an inherited lock from a subsection
|
||||
Given I have a course with sections, subsections, and units
|
||||
And I have enabled explicit staff lock on a subsection and one of its units
|
||||
When I click on the subsection's configuration icon
|
||||
And I disable explicit staff locking
|
||||
And I click save
|
||||
Then the unit still shows a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
unit = subsection.unit_at(0)
|
||||
self._verify_explicit_staff_lock_remains_after_unlocking_parent(unit, subsection)
|
||||
|
||||
def test_section_displays_lock_when_all_subsections_locked(self):
|
||||
"""
|
||||
Scenario: All subsections in section are explicitly locked, section should display staff only warning
|
||||
Given I have a course one section and two subsections
|
||||
When I enable explicit staff lock on all the subsections
|
||||
Then the section shows a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
section.subsection_at(0).set_staff_lock(True)
|
||||
section.subsection_at(1).set_staff_lock(True)
|
||||
self.assertTrue(section.has_staff_lock_warning)
|
||||
|
||||
def test_section_displays_lock_when_all_units_locked(self):
|
||||
"""
|
||||
Scenario: All units in a section are explicitly locked, section should display staff only warning
|
||||
Given I have a course with one section, two subsections, and four units
|
||||
When I enable explicit staff lock on all the units
|
||||
Then the section shows a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
section.subsection_at(0).unit_at(0).set_staff_lock(True)
|
||||
section.subsection_at(0).unit_at(1).set_staff_lock(True)
|
||||
section.subsection_at(1).unit_at(0).set_staff_lock(True)
|
||||
section.subsection_at(1).unit_at(1).set_staff_lock(True)
|
||||
self.assertTrue(section.has_staff_lock_warning)
|
||||
|
||||
def test_subsection_displays_lock_when_all_units_locked(self):
|
||||
"""
|
||||
Scenario: All units in subsection are explicitly locked, subsection should display staff only warning
|
||||
Given I have a course with one subsection and two units
|
||||
When I enable explicit staff lock on all the units
|
||||
Then the subsection shows a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
subsection.unit_at(0).set_staff_lock(True)
|
||||
subsection.unit_at(1).set_staff_lock(True)
|
||||
self.assertTrue(subsection.has_staff_lock_warning)
|
||||
|
||||
def test_section_does_not_display_lock_when_some_subsections_locked(self):
|
||||
"""
|
||||
Scenario: Only some subsections in section are explicitly locked, section should NOT display staff only warning
|
||||
Given I have a course with one section and two subsections
|
||||
When I enable explicit staff lock on one subsection
|
||||
Then the section does not show a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
section.subsection_at(0).set_staff_lock(True)
|
||||
self.assertFalse(section.has_staff_lock_warning)
|
||||
|
||||
def test_section_does_not_display_lock_when_some_units_locked(self):
|
||||
"""
|
||||
Scenario: Only some units in section are explicitly locked, section should NOT display staff only warning
|
||||
Given I have a course with one section, two subsections, and four units
|
||||
When I enable explicit staff lock on three units
|
||||
Then the section does not show a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
section.subsection_at(0).unit_at(0).set_staff_lock(True)
|
||||
section.subsection_at(0).unit_at(1).set_staff_lock(True)
|
||||
section.subsection_at(1).unit_at(1).set_staff_lock(True)
|
||||
self.assertFalse(section.has_staff_lock_warning)
|
||||
|
||||
def test_subsection_does_not_display_lock_when_some_units_locked(self):
|
||||
"""
|
||||
Scenario: Only some units in subsection are explicitly locked, subsection should NOT display staff only warning
|
||||
Given I have a course with one subsection and two units
|
||||
When I enable explicit staff lock on one unit
|
||||
Then the subsection does not show a staff lock warning
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
subsection.unit_at(0).set_staff_lock(True)
|
||||
self.assertFalse(subsection.has_staff_lock_warning)
|
||||
|
||||
def test_locked_sections_do_not_appear_in_lms(self):
|
||||
"""
|
||||
Scenario: A locked section is not visible to students in the LMS
|
||||
Given I have a course with two sections
|
||||
When I enable explicit staff lock on one section
|
||||
And I click the View Live button to switch to staff view
|
||||
Then I see two sections in the sidebar
|
||||
And when I click to toggle to student view
|
||||
Then I see one section in the sidebar
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.add_section_from_top_button()
|
||||
self.course_outline_page.section_at(1).set_staff_lock(True)
|
||||
self.course_outline_page.view_live()
|
||||
courseware = CoursewarePage(self.browser, self.course_id)
|
||||
courseware.wait_for_page()
|
||||
self.assertEqual(courseware.num_sections, 2)
|
||||
StaffPage(self.browser).toggle_staff_view()
|
||||
self.assertEqual(courseware.num_sections, 1)
|
||||
|
||||
def test_locked_subsections_do_not_appear_in_lms(self):
|
||||
"""
|
||||
Scenario: A locked subsection is not visible to students in the LMS
|
||||
Given I have a course with two subsections
|
||||
When I enable explicit staff lock on one subsection
|
||||
And I click the View Live button to switch to staff view
|
||||
Then I see two subsections in the sidebar
|
||||
And when I click to toggle to student view
|
||||
Then I see one section in the sidebar
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.section_at(0).subsection_at(1).set_staff_lock(True)
|
||||
self.course_outline_page.view_live()
|
||||
courseware = CoursewarePage(self.browser, self.course_id)
|
||||
courseware.wait_for_page()
|
||||
self.assertEqual(courseware.num_subsections, 2)
|
||||
StaffPage(self.browser).toggle_staff_view()
|
||||
self.assertEqual(courseware.num_subsections, 1)
|
||||
|
||||
def test_toggling_staff_lock_on_section_does_not_publish_draft_units(self):
|
||||
"""
|
||||
Scenario: Locking and unlocking a section will not publish its draft units
|
||||
Given I have a course with a section and unit
|
||||
And the unit has a draft and published version
|
||||
When I enable explicit staff lock on the section
|
||||
And I disable explicit staff lock on the section
|
||||
And I click the View Live button to switch to staff view
|
||||
Then I see the published version of the unit
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to()
|
||||
add_discussion(unit)
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
section.set_staff_lock(True)
|
||||
section.set_staff_lock(False)
|
||||
unit = section.subsection_at(0).unit_at(0).go_to()
|
||||
unit.view_published_version()
|
||||
courseware = CoursewarePage(self.browser, self.course_id)
|
||||
courseware.wait_for_page()
|
||||
self.assertEqual(courseware.num_xblock_components, 0)
|
||||
|
||||
def test_toggling_staff_lock_on_subsection_does_not_publish_draft_units(self):
|
||||
"""
|
||||
Scenario: Locking and unlocking a subsection will not publish its draft units
|
||||
Given I have a course with a subsection and unit
|
||||
And the unit has a draft and published version
|
||||
When I enable explicit staff lock on the subsection
|
||||
And I disable explicit staff lock on the subsection
|
||||
And I click the View Live button to switch to staff view
|
||||
Then I see the published version of the unit
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0).go_to()
|
||||
add_discussion(unit)
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
subsection.set_staff_lock(True)
|
||||
subsection.set_staff_lock(False)
|
||||
unit = subsection.unit_at(0).go_to()
|
||||
unit.view_published_version()
|
||||
courseware = CoursewarePage(self.browser, self.course_id)
|
||||
courseware.wait_for_page()
|
||||
self.assertEqual(courseware.num_xblock_components, 0)
|
||||
|
||||
def test_removing_staff_lock_from_unit_without_inherited_lock_shows_warning(self):
|
||||
"""
|
||||
Scenario: Removing explicit staff lock from a unit which does not inherit staff lock displays a warning.
|
||||
Given I have a course with a subsection and unit
|
||||
When I enable explicit staff lock on the unit
|
||||
And I disable explicit staff lock on the unit
|
||||
Then I see a modal warning.
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
unit = self.course_outline_page.section_at(0).subsection_at(0).unit_at(0)
|
||||
unit.set_staff_lock(True)
|
||||
self._remove_staff_lock_and_verify_warning(unit, True)
|
||||
|
||||
def test_removing_staff_lock_from_subsection_without_inherited_lock_shows_warning(self):
|
||||
"""
|
||||
Scenario: Removing explicit staff lock from a subsection which does not inherit staff lock displays a warning.
|
||||
Given I have a course with a section and subsection
|
||||
When I enable explicit staff lock on the subsection
|
||||
And I disable explicit staff lock on the subsection
|
||||
Then I see a modal warning.
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
subsection.set_staff_lock(True)
|
||||
self._remove_staff_lock_and_verify_warning(subsection, True)
|
||||
|
||||
def test_removing_staff_lock_from_unit_with_inherited_lock_shows_no_warning(self):
|
||||
"""
|
||||
Scenario: Removing explicit staff lock from a unit which also inherits staff lock displays no warning.
|
||||
Given I have a course with a subsection and unit
|
||||
When I enable explicit staff lock on the subsection
|
||||
And I enable explicit staff lock on the unit
|
||||
When I disable explicit staff lock on the unit
|
||||
Then I do not see a modal warning.
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
subsection = self.course_outline_page.section_at(0).subsection_at(0)
|
||||
unit = subsection.unit_at(0)
|
||||
subsection.set_staff_lock(True)
|
||||
unit.set_staff_lock(True)
|
||||
self._remove_staff_lock_and_verify_warning(unit, False)
|
||||
|
||||
def test_removing_staff_lock_from_subsection_with_inherited_lock_shows_no_warning(self):
|
||||
"""
|
||||
Scenario: Removing explicit staff lock from a subsection which also inherits staff lock displays no warning.
|
||||
Given I have a course with a section and subsection
|
||||
When I enable explicit staff lock on the section
|
||||
And I enable explicit staff lock on the subsection
|
||||
When I disable explicit staff lock on the subsection
|
||||
Then I do not see a modal warning.
|
||||
"""
|
||||
self.course_outline_page.visit()
|
||||
self.course_outline_page.expand_all_subsections()
|
||||
section = self.course_outline_page.section_at(0)
|
||||
subsection = section.subsection_at(0)
|
||||
section.set_staff_lock(True)
|
||||
subsection.set_staff_lock(True)
|
||||
self._remove_staff_lock_and_verify_warning(subsection, False)
|
||||
|
||||
|
||||
@attr('shard_2')
|
||||
class EditNamesTest(CourseOutlineTest):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user