This will remove imports from __future__ that are no longer needed. https://docs.python.org/3.5/library/2to3.html#2to3fixer-future
1522 lines
67 KiB
Python
1522 lines
67 KiB
Python
"""
|
|
Acceptance tests for Studio related to the container page.
|
|
The container page is used both for displaying units, and
|
|
for displaying containers within units.
|
|
"""
|
|
|
|
|
|
import datetime
|
|
|
|
import ddt
|
|
import six
|
|
|
|
from common.test.acceptance.fixtures.course import XBlockFixtureDesc
|
|
from common.test.acceptance.pages.lms.courseware import CoursewarePage
|
|
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
|
|
from common.test.acceptance.pages.lms.staff_view import StaffCoursewarePage
|
|
from common.test.acceptance.pages.studio.container import ContainerPage
|
|
from common.test.acceptance.pages.studio.html_component_editor import HtmlXBlockEditorView
|
|
from common.test.acceptance.pages.studio.move_xblock import MoveModalView
|
|
from common.test.acceptance.pages.studio.utils import add_discussion
|
|
from common.test.acceptance.pages.studio.xblock_editor import XBlockEditorView, XBlockVisibilityEditorView
|
|
from common.test.acceptance.tests.helpers import create_user_partition_json
|
|
from openedx.core.lib.tests import attr
|
|
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID, MINIMUM_STATIC_PARTITION_ID, Group
|
|
|
|
from .base_studio_test import ContainerBase
|
|
|
|
|
|
class NestedVerticalTest(ContainerBase):
|
|
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Sets up a course structure with nested verticals.
|
|
"""
|
|
self.container_title = ""
|
|
self.group_a = "Group A"
|
|
self.group_b = "Group B"
|
|
self.group_empty = "Group Empty"
|
|
self.group_a_item_1 = "Group A Item 1"
|
|
self.group_a_item_2 = "Group A Item 2"
|
|
self.group_b_item_1 = "Group B Item 1"
|
|
self.group_b_item_2 = "Group B Item 2"
|
|
|
|
self.group_a_handle = 0
|
|
self.group_a_item_1_handle = 1
|
|
self.group_a_item_2_handle = 2
|
|
self.group_empty_handle = 3
|
|
self.group_b_handle = 4
|
|
self.group_b_item_1_handle = 5
|
|
self.group_b_item_2_handle = 6
|
|
|
|
self.group_a_item_1_action_index = 0
|
|
self.group_a_item_2_action_index = 1
|
|
|
|
self.duplicate_label = u"Duplicate of '{0}'"
|
|
self.discussion_label = "Discussion"
|
|
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Container').add_children(
|
|
XBlockFixtureDesc('vertical', 'Group A').add_children(
|
|
XBlockFixtureDesc('html', self.group_a_item_1),
|
|
XBlockFixtureDesc('html', self.group_a_item_2)
|
|
),
|
|
XBlockFixtureDesc('vertical', 'Group Empty'),
|
|
XBlockFixtureDesc('vertical', 'Group B').add_children(
|
|
XBlockFixtureDesc('html', self.group_b_item_1),
|
|
XBlockFixtureDesc('html', self.group_b_item_2)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
|
|
@attr(shard=1)
|
|
class AddComponentTest(NestedVerticalTest):
|
|
"""
|
|
Tests of adding a component to the container page.
|
|
"""
|
|
|
|
def add_and_verify(self, menu_index, expected_ordering):
|
|
self.do_action_and_verify(
|
|
lambda container: add_discussion(container, menu_index),
|
|
expected_ordering
|
|
)
|
|
|
|
def test_add_component_in_group(self):
|
|
group_b_menu = 2
|
|
|
|
expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]},
|
|
{self.group_a: [self.group_a_item_1, self.group_a_item_2]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2, self.discussion_label]},
|
|
{self.group_empty: []}]
|
|
self.add_and_verify(group_b_menu, expected_ordering)
|
|
|
|
def test_add_component_in_empty_group(self):
|
|
group_empty_menu = 1
|
|
|
|
expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]},
|
|
{self.group_a: [self.group_a_item_1, self.group_a_item_2]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
|
{self.group_empty: [self.discussion_label]}]
|
|
self.add_and_verify(group_empty_menu, expected_ordering)
|
|
|
|
def test_add_component_in_container(self):
|
|
container_menu = 3
|
|
|
|
expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b, self.discussion_label]},
|
|
{self.group_a: [self.group_a_item_1, self.group_a_item_2]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
|
{self.group_empty: []}]
|
|
self.add_and_verify(container_menu, expected_ordering)
|
|
|
|
|
|
@attr(shard=1)
|
|
class DuplicateComponentTest(NestedVerticalTest):
|
|
"""
|
|
Tests of duplicating a component on the container page.
|
|
"""
|
|
|
|
def duplicate_and_verify(self, source_index, expected_ordering):
|
|
self.do_action_and_verify(
|
|
lambda container: container.duplicate(source_index),
|
|
expected_ordering
|
|
)
|
|
|
|
def test_duplicate_first_in_group(self):
|
|
duplicate_label = self.duplicate_label.format(self.group_a_item_1)
|
|
expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]},
|
|
{self.group_a: [self.group_a_item_1, duplicate_label, self.group_a_item_2]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
|
{self.group_empty: []}]
|
|
self.duplicate_and_verify(self.group_a_item_1_action_index, expected_ordering)
|
|
|
|
def test_duplicate_second_in_group(self):
|
|
duplicate_label = self.duplicate_label.format(self.group_a_item_2)
|
|
expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]},
|
|
{self.group_a: [self.group_a_item_1, self.group_a_item_2, duplicate_label]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
|
{self.group_empty: []}]
|
|
self.duplicate_and_verify(self.group_a_item_2_action_index, expected_ordering)
|
|
|
|
def test_duplicate_the_duplicate(self):
|
|
first_duplicate_label = self.duplicate_label.format(self.group_a_item_1)
|
|
second_duplicate_label = self.duplicate_label.format(first_duplicate_label)
|
|
|
|
expected_ordering = [
|
|
{self.container_title: [self.group_a, self.group_empty, self.group_b]},
|
|
{self.group_a: [self.group_a_item_1, first_duplicate_label, second_duplicate_label, self.group_a_item_2]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
|
{self.group_empty: []}
|
|
]
|
|
|
|
def duplicate_twice(container):
|
|
container.duplicate(self.group_a_item_1_action_index)
|
|
container.duplicate(self.group_a_item_1_action_index + 1)
|
|
|
|
self.do_action_and_verify(duplicate_twice, expected_ordering)
|
|
|
|
|
|
@attr(shard=1)
|
|
class DeleteComponentTest(NestedVerticalTest):
|
|
"""
|
|
Tests of deleting a component from the container page.
|
|
"""
|
|
|
|
def delete_and_verify(self, source_index, expected_ordering):
|
|
self.do_action_and_verify(
|
|
lambda container: container.delete(source_index),
|
|
expected_ordering
|
|
)
|
|
|
|
def test_delete_first_in_group(self):
|
|
expected_ordering = [{self.container_title: [self.group_a, self.group_empty, self.group_b]},
|
|
{self.group_a: [self.group_a_item_2]},
|
|
{self.group_b: [self.group_b_item_1, self.group_b_item_2]},
|
|
{self.group_empty: []}]
|
|
|
|
# Group A itself has a delete icon now, so item_1 is index 1 instead of 0.
|
|
group_a_item_1_delete_index = 1
|
|
self.delete_and_verify(group_a_item_1_delete_index, expected_ordering)
|
|
|
|
|
|
@attr(shard=16)
|
|
class EditContainerTest(NestedVerticalTest):
|
|
"""
|
|
Tests of editing a container.
|
|
"""
|
|
|
|
def modify_display_name_and_verify(self, component):
|
|
"""
|
|
Helper method for changing a display name.
|
|
"""
|
|
modified_name = 'modified'
|
|
self.assertNotEqual(component.name, modified_name)
|
|
component.edit()
|
|
component_editor = XBlockEditorView(self.browser, component.locator)
|
|
component_editor.set_field_value_and_save('Display Name', modified_name)
|
|
self.assertEqual(component.name, modified_name)
|
|
|
|
def test_edit_container_on_unit_page(self):
|
|
"""
|
|
Test the "edit" button on a container appearing on the unit page.
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
component = unit.xblocks[1]
|
|
self.modify_display_name_and_verify(component)
|
|
|
|
def test_edit_container_on_container_page(self):
|
|
"""
|
|
Test the "edit" button on a container appearing on the container page.
|
|
"""
|
|
container = self.go_to_nested_container_page()
|
|
self.modify_display_name_and_verify(container)
|
|
|
|
|
|
class BaseGroupConfigurationsTest(ContainerBase):
|
|
ALL_LEARNERS_AND_STAFF = XBlockVisibilityEditorView.ALL_LEARNERS_AND_STAFF
|
|
CHOOSE_ONE = "Select a group type"
|
|
CONTENT_GROUP_PARTITION = XBlockVisibilityEditorView.CONTENT_GROUP_PARTITION
|
|
ENROLLMENT_TRACK_PARTITION = XBlockVisibilityEditorView.ENROLLMENT_TRACK_PARTITION
|
|
MISSING_GROUP_LABEL = 'Deleted Group\nThis group no longer exists. Choose another group or remove the access restriction.'
|
|
VALIDATION_ERROR_LABEL = 'This component has validation issues.'
|
|
VALIDATION_ERROR_MESSAGE = "Error:\nThis component's access settings refer to deleted or invalid groups."
|
|
GROUP_VISIBILITY_MESSAGE = 'Access to some content in this unit is restricted to specific groups of learners.'
|
|
MODAL_NOT_RESTRICTED_MESSAGE = "Access is not restricted"
|
|
|
|
def setUp(self):
|
|
super(BaseGroupConfigurationsTest, self).setUp()
|
|
|
|
# Set up a cohort-schemed user partition
|
|
self.id_base = MINIMUM_STATIC_PARTITION_ID
|
|
self.course_fixture._update_xblock(self.course_fixture._course_location, {
|
|
"metadata": {
|
|
u"user_partitions": [
|
|
create_user_partition_json(
|
|
self.id_base,
|
|
self.CONTENT_GROUP_PARTITION,
|
|
'Content Group Partition',
|
|
[
|
|
Group(self.id_base + 1, 'Dogs'),
|
|
Group(self.id_base + 2, 'Cats')
|
|
],
|
|
scheme="cohort"
|
|
)
|
|
],
|
|
},
|
|
})
|
|
|
|
self.container_page = self.go_to_unit_page()
|
|
self.html_component = self.container_page.xblocks[1]
|
|
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Populate a simple course a section, subsection, and unit, and HTML component.
|
|
"""
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
|
XBlockFixtureDesc('html', 'Html Component')
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
def edit_component_visibility(self, component):
|
|
"""
|
|
Edit the visibility of an xblock on the container page and returns an XBlockVisibilityEditorView.
|
|
"""
|
|
component.edit_visibility()
|
|
return XBlockVisibilityEditorView(self.browser, component.locator)
|
|
|
|
def edit_unit_visibility(self, unit):
|
|
"""
|
|
Edit the visibility of a unit on the container page and returns an XBlockVisibilityEditorView.
|
|
"""
|
|
unit.edit_visibility()
|
|
return XBlockVisibilityEditorView(self.browser, unit.locator)
|
|
|
|
def verify_current_groups_message(self, visibility_editor, expected_current_groups):
|
|
"""
|
|
Check that the current visibility is displayed at the top of the dialog.
|
|
"""
|
|
if expected_current_groups == self.ALL_LEARNERS_AND_STAFF:
|
|
self.assertEqual("Access is not restricted", visibility_editor.current_groups_message)
|
|
else:
|
|
self.assertEqual(
|
|
u"Access is restricted to: {groups}".format(groups=expected_current_groups),
|
|
visibility_editor.current_groups_message
|
|
)
|
|
|
|
def verify_selected_partition_scheme(self, visibility_editor, expected_scheme):
|
|
"""
|
|
Check that the expected partition scheme is selected.
|
|
"""
|
|
six.assertCountEqual(self, expected_scheme, visibility_editor.selected_partition_scheme)
|
|
|
|
def verify_selected_groups(self, visibility_editor, expected_groups):
|
|
"""
|
|
Check the expected partition groups.
|
|
"""
|
|
six.assertCountEqual(self, expected_groups, [group.text for group in visibility_editor.selected_groups])
|
|
|
|
def select_and_verify_saved(self, component, partition_label, groups=[]):
|
|
"""
|
|
Edit the visibility of an xblock on the container page and
|
|
verify that the edit persists. Note that `groups`
|
|
are labels which should be clicked, but not necessarily checked.
|
|
"""
|
|
# Make initial edit(s) and save
|
|
visibility_editor = self.edit_component_visibility(component)
|
|
|
|
visibility_editor.select_groups_in_partition_scheme(partition_label, groups)
|
|
|
|
# Re-open the modal and inspect its selected inputs. If no groups were selected,
|
|
# "All Learners" should be selected partitions scheme, and we show "Select a group type" in the select.
|
|
if not groups:
|
|
partition_label = self.CHOOSE_ONE
|
|
visibility_editor = self.edit_component_visibility(component)
|
|
self.verify_selected_partition_scheme(visibility_editor, partition_label)
|
|
self.verify_selected_groups(visibility_editor, groups)
|
|
visibility_editor.save()
|
|
|
|
def select_and_verify_unit_group_access(self, unit, partition_label, groups=[]):
|
|
"""
|
|
Edit the visibility of an xblock on the unit page and
|
|
verify that the edit persists. Note that `groups`
|
|
are labels which should be clicked, but are not necessarily checked.
|
|
"""
|
|
unit_access_editor = self.edit_unit_visibility(unit)
|
|
unit_access_editor.select_groups_in_partition_scheme(partition_label, groups)
|
|
|
|
if not groups:
|
|
partition_label = self.CHOOSE_ONE
|
|
unit_access_editor = self.edit_unit_visibility(unit)
|
|
self.verify_selected_partition_scheme(unit_access_editor, partition_label)
|
|
self.verify_selected_groups(unit_access_editor, groups)
|
|
unit_access_editor.save()
|
|
|
|
def verify_component_validation_error(self, component):
|
|
"""
|
|
Verify that we see validation errors for the given component.
|
|
"""
|
|
self.assertTrue(component.has_validation_error)
|
|
self.assertEqual(component.validation_error_text, self.VALIDATION_ERROR_LABEL)
|
|
self.assertEqual([self.VALIDATION_ERROR_MESSAGE], component.validation_error_messages)
|
|
|
|
def verify_visibility_set(self, component, is_set):
|
|
"""
|
|
Verify that the container page shows that component visibility
|
|
settings have been edited if `is_set` is True; otherwise
|
|
verify that the container page shows no such information.
|
|
"""
|
|
if is_set:
|
|
self.assertIn(self.GROUP_VISIBILITY_MESSAGE, self.container_page.sidebar_visibility_message)
|
|
self.assertTrue(component.has_group_visibility_set)
|
|
else:
|
|
self.assertNotIn(self.GROUP_VISIBILITY_MESSAGE, self.container_page.sidebar_visibility_message)
|
|
self.assertFalse(component.has_group_visibility_set)
|
|
|
|
def verify_unit_visibility_set(self, unit, set_groups=[]):
|
|
"""
|
|
Verify that the container visibility modal shows that unit visibility
|
|
settings have been edited if there are `set_groups`. Otherwise verify
|
|
that the modal shows no such information.
|
|
"""
|
|
unit_access_editor = self.edit_unit_visibility(unit)
|
|
if set_groups:
|
|
self.assertIn(", ".join(set_groups), unit_access_editor.current_groups_message)
|
|
else:
|
|
self.assertEqual(self.MODAL_NOT_RESTRICTED_MESSAGE, unit_access_editor.current_groups_message)
|
|
unit_access_editor.cancel()
|
|
|
|
def update_component(self, component, metadata):
|
|
"""
|
|
Update a component's metadata and refresh the page.
|
|
"""
|
|
self.course_fixture._update_xblock(component.locator, {'metadata': metadata})
|
|
self.browser.refresh()
|
|
self.container_page.wait_for_page()
|
|
|
|
def remove_missing_groups(self, visibility_editor, component):
|
|
"""
|
|
Deselect the missing groups for a component. After save,
|
|
verify that there are no missing group messages in the modal
|
|
and that there is no validation error on the component.
|
|
"""
|
|
for option in visibility_editor.all_group_options:
|
|
if option.text == self.MISSING_GROUP_LABEL:
|
|
option.click()
|
|
visibility_editor.save()
|
|
visibility_editor = self.edit_component_visibility(component)
|
|
self.assertNotIn(self.MISSING_GROUP_LABEL, [item.text for item in visibility_editor.all_group_options])
|
|
visibility_editor.cancel()
|
|
self.assertFalse(component.has_validation_error)
|
|
|
|
|
|
@attr(shard=21)
|
|
class UnitAccessContainerTest(BaseGroupConfigurationsTest):
|
|
"""
|
|
Tests unit level access
|
|
"""
|
|
GROUP_RESTRICTED_MESSAGE = 'Access to this unit is restricted to: Dogs'
|
|
|
|
def _toggle_container_unit_access(self, group_ids, unit):
|
|
"""
|
|
Toggle the unit level access on the course outline page
|
|
"""
|
|
unit.toggle_unit_access('Content Groups', group_ids)
|
|
|
|
def _verify_container_unit_access_message(self, group_ids, expected_message):
|
|
"""
|
|
Check that the container page displays the correct unit
|
|
access message.
|
|
"""
|
|
self.outline.visit()
|
|
self.outline.expand_all_subsections()
|
|
unit = self.outline.section_at(0).subsection_at(0).unit_at(0)
|
|
self._toggle_container_unit_access(group_ids, unit)
|
|
|
|
container_page = self.go_to_unit_page()
|
|
self.assertEqual(str(container_page.get_xblock_access_message()), expected_message)
|
|
|
|
def test_default_selection(self):
|
|
"""
|
|
Tests that no message is displayed when there are no
|
|
restrictions on the unit or components.
|
|
"""
|
|
self._verify_container_unit_access_message([], '')
|
|
|
|
def test_restricted_components_message(self):
|
|
"""
|
|
Test that the proper message is displayed when access to
|
|
some components is restricted.
|
|
"""
|
|
container_page = self.go_to_unit_page()
|
|
html_component = container_page.xblocks[1]
|
|
|
|
# Initially set visibility to Dog group.
|
|
self.update_component(
|
|
html_component,
|
|
{'group_access': {self.id_base: [self.id_base + 1]}}
|
|
)
|
|
|
|
self._verify_container_unit_access_message([], self.GROUP_VISIBILITY_MESSAGE)
|
|
|
|
def test_restricted_access_message(self):
|
|
"""
|
|
Test that the proper message is displayed when access to the
|
|
unit is restricted to a particular group.
|
|
"""
|
|
self._verify_container_unit_access_message([self.id_base + 1], self.GROUP_RESTRICTED_MESSAGE)
|
|
|
|
|
|
@attr(shard=9)
|
|
class ContentGroupVisibilityModalTest(BaseGroupConfigurationsTest):
|
|
"""
|
|
Tests of the visibility settings modal for components on the unit
|
|
page (content groups).
|
|
"""
|
|
def test_default_selection(self):
|
|
"""
|
|
Scenario: The component visibility modal selects visible to all by default.
|
|
Given I have a unit with one component
|
|
When I go to the container page for that unit
|
|
And I open the visibility editor modal for that unit's component
|
|
Then the default visibility selection should be 'All Students and Staff'
|
|
And the container page should not display the content visibility warning
|
|
"""
|
|
visibility_dialog = self.edit_component_visibility(self.html_component)
|
|
self.verify_current_groups_message(visibility_dialog, self.ALL_LEARNERS_AND_STAFF)
|
|
self.verify_selected_partition_scheme(visibility_dialog, self.CHOOSE_ONE)
|
|
visibility_dialog.cancel()
|
|
self.verify_visibility_set(self.html_component, False)
|
|
|
|
def test_reset_to_all_students_and_staff(self):
|
|
"""
|
|
Scenario: The component visibility modal can be set to be visible to all students and staff.
|
|
Given I have a unit with one component
|
|
When I go to the container page for that unit
|
|
Then the container page should not display the content visibility warning by default.
|
|
If I then restrict access and save, and then I open the visibility editor modal for that unit's component
|
|
And I select 'All Students and Staff'
|
|
And I save the modal
|
|
Then the visibility selection should be 'All Students and Staff'
|
|
And the container page should still not display the content visibility warning
|
|
"""
|
|
self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs'])
|
|
self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF)
|
|
self.verify_visibility_set(self.html_component, False)
|
|
|
|
def test_reset_unit_access_to_all_students_and_staff(self):
|
|
"""
|
|
Scenario: The unit visibility modal can be set to be visible to all students and staff.
|
|
Given I have a unit
|
|
When I go to the container page for that unit
|
|
And I open the visibility editor modal for that unit
|
|
And I select 'Dogs'
|
|
And I save the modal
|
|
Then I re-open the modal, the unit access modal should display the content visibility settings
|
|
Then after re-opening the modal again
|
|
And I select 'All Learners and Staff'
|
|
And I save the modal
|
|
And I re-open the modal, the unit access modal should display that no content is restricted
|
|
"""
|
|
self.select_and_verify_unit_group_access(self.container_page, self.CONTENT_GROUP_PARTITION, ['Dogs'])
|
|
self.verify_unit_visibility_set(self.container_page, set_groups=["Dogs"])
|
|
self.select_and_verify_unit_group_access(self.container_page, self.ALL_LEARNERS_AND_STAFF)
|
|
self.verify_unit_visibility_set(self.container_page)
|
|
|
|
def test_select_single_content_group(self):
|
|
"""
|
|
Scenario: The component visibility modal can be set to be visible to one content group.
|
|
Given I have a unit with one component
|
|
When I go to the container page for that unit
|
|
And I open the visibility editor modal for that unit's component
|
|
And I select 'Dogs'
|
|
And I save the modal
|
|
Then the visibility selection should be 'Dogs' and 'Specific Content Groups'
|
|
"""
|
|
self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs'])
|
|
|
|
def test_select_multiple_content_groups(self):
|
|
"""
|
|
Scenario: The component visibility modal can be set to be visible to multiple content groups.
|
|
Given I have a unit with one component
|
|
When I go to the container page for that unit
|
|
And I open the visibility editor modal for that unit's component
|
|
And I select 'Dogs' and 'Cats'
|
|
And I save the modal
|
|
Then the visibility selection should be 'Dogs', 'Cats', and 'Specific Content Groups'
|
|
"""
|
|
self.select_and_verify_saved(self.html_component, self.CONTENT_GROUP_PARTITION, ['Dogs', 'Cats'])
|
|
|
|
def test_select_zero_content_groups(self):
|
|
"""
|
|
Scenario: The component visibility modal can not be set to be visible to 'Specific Content Groups' without
|
|
selecting those specific groups.
|
|
Given I have a unit with one component
|
|
When I go to the container page for that unit
|
|
And I open the visibility editor modal for that unit's component
|
|
And I select 'Specific Content Groups'
|
|
And I save the modal
|
|
Then the visibility selection should be 'All Students and Staff'
|
|
And the container page should not display the content visibility warning
|
|
"""
|
|
self.select_and_verify_saved(
|
|
self.html_component, self.CONTENT_GROUP_PARTITION
|
|
)
|
|
self.verify_visibility_set(self.html_component, False)
|
|
|
|
def test_missing_groups(self):
|
|
"""
|
|
Scenario: The component visibility modal shows a validation error when visibility is set to multiple unknown
|
|
group ids.
|
|
Given I have a unit with one component
|
|
And that component's group access specifies multiple invalid group ids
|
|
When I go to the container page for that unit
|
|
Then I should see a validation error message on that unit's component
|
|
And I open the visibility editor modal for that unit's component
|
|
Then I should see that I have selected multiple deleted groups
|
|
And the container page should display the content visibility warning
|
|
And I de-select the missing groups
|
|
And I save the modal
|
|
Then the visibility selection should be 'All Students and Staff'
|
|
And I should not see any validation errors on the component
|
|
And the container page should not display the content visibility warning
|
|
"""
|
|
self.update_component(
|
|
self.html_component,
|
|
{'group_access': {self.id_base: [self.id_base + 3, self.id_base + 4]}}
|
|
)
|
|
self._verify_and_remove_missing_content_groups(
|
|
"Deleted Group, Deleted Group",
|
|
[self.MISSING_GROUP_LABEL] * 2
|
|
)
|
|
self.verify_visibility_set(self.html_component, False)
|
|
|
|
def test_found_and_missing_groups(self):
|
|
"""
|
|
Scenario: The component visibility modal shows a validation error when visibility is set to multiple unknown
|
|
group ids and multiple known group ids.
|
|
Given I have a unit with one component
|
|
And that component's group access specifies multiple invalid and valid group ids
|
|
When I go to the container page for that unit
|
|
Then I should see a validation error message on that unit's component
|
|
And I open the visibility editor modal for that unit's component
|
|
Then I should see that I have selected multiple deleted groups
|
|
And then if I de-select the missing groups
|
|
And I save the modal
|
|
Then the visibility selection should be the names of the valid groups.
|
|
And I should not see any validation errors on the component
|
|
"""
|
|
self.update_component(
|
|
self.html_component,
|
|
{'group_access': {self.id_base: [self.id_base + 1, self.id_base + 2, self.id_base + 3, self.id_base + 4]}}
|
|
)
|
|
|
|
self._verify_and_remove_missing_content_groups(
|
|
'Dogs, Cats, Deleted Group, Deleted Group',
|
|
['Dogs', 'Cats'] + [self.MISSING_GROUP_LABEL] * 2
|
|
)
|
|
|
|
visibility_editor = self.edit_component_visibility(self.html_component)
|
|
self.verify_selected_partition_scheme(visibility_editor, self.CONTENT_GROUP_PARTITION)
|
|
expected_groups = ['Dogs', 'Cats']
|
|
self.verify_current_groups_message(visibility_editor, ", ".join(expected_groups))
|
|
self.verify_selected_groups(visibility_editor, expected_groups)
|
|
|
|
def _verify_and_remove_missing_content_groups(self, current_groups_message, all_group_labels):
|
|
self.verify_component_validation_error(self.html_component)
|
|
visibility_editor = self.edit_component_visibility(self.html_component)
|
|
self.verify_selected_partition_scheme(visibility_editor, self.CONTENT_GROUP_PARTITION)
|
|
self.verify_current_groups_message(visibility_editor, current_groups_message)
|
|
self.verify_selected_groups(visibility_editor, all_group_labels)
|
|
self.remove_missing_groups(visibility_editor, self.html_component)
|
|
|
|
|
|
@attr(shard=3)
|
|
class EnrollmentTrackVisibilityModalTest(BaseGroupConfigurationsTest):
|
|
"""
|
|
Tests of the visibility settings modal for components on the unit
|
|
page (enrollment tracks).
|
|
"""
|
|
AUDIT_TRACK = "Audit Track"
|
|
VERIFIED_TRACK = "Verified Track"
|
|
|
|
def setUp(self):
|
|
super(EnrollmentTrackVisibilityModalTest, self).setUp()
|
|
|
|
# Add an audit mode to the course
|
|
ModeCreationPage(self.browser, self.course_id, mode_slug=u'audit', mode_display_name=self.AUDIT_TRACK).visit()
|
|
|
|
# Add a verified mode to the course
|
|
ModeCreationPage(
|
|
self.browser, self.course_id, mode_slug=u'verified',
|
|
mode_display_name=self.VERIFIED_TRACK, min_price=10
|
|
).visit()
|
|
|
|
self.container_page = self.go_to_unit_page()
|
|
self.html_component = self.container_page.xblocks[1]
|
|
|
|
# Initially set visibility to Verified track.
|
|
self.update_component(
|
|
self.html_component,
|
|
{'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [2]}} # "2" is Verified
|
|
)
|
|
|
|
def verify_component_group_visibility_messsage(self, component, expected_groups):
|
|
"""
|
|
Verifies that the group visibility message below the component display name is correct.
|
|
"""
|
|
if not expected_groups:
|
|
self.assertIsNone(component.get_partition_group_message)
|
|
else:
|
|
self.assertEqual("Access restricted to: " + expected_groups, component.get_partition_group_message)
|
|
|
|
def test_setting_enrollment_tracks(self):
|
|
"""
|
|
Test that enrollment track groups can be selected.
|
|
"""
|
|
# Verify that the "Verified" Group is shown on the unit page (under the unit display name).
|
|
self.verify_component_group_visibility_messsage(self.html_component, "Verified Track")
|
|
|
|
# Open dialog with "Verified" already selected.
|
|
visibility_editor = self.edit_component_visibility(self.html_component)
|
|
self.verify_current_groups_message(visibility_editor, self.VERIFIED_TRACK)
|
|
self.verify_selected_partition_scheme(
|
|
visibility_editor,
|
|
self.ENROLLMENT_TRACK_PARTITION
|
|
)
|
|
self.verify_selected_groups(visibility_editor, [self.VERIFIED_TRACK])
|
|
visibility_editor.cancel()
|
|
|
|
# Select "All Learners and Staff". The helper method saves the change,
|
|
# then reopens the dialog to verify that it was persisted.
|
|
self.select_and_verify_saved(self.html_component, self.ALL_LEARNERS_AND_STAFF)
|
|
self.verify_component_group_visibility_messsage(self.html_component, None)
|
|
|
|
# Select "Audit" enrollment track. The helper method saves the change,
|
|
# then reopens the dialog to verify that it was persisted.
|
|
self.select_and_verify_saved(self.html_component, self.ENROLLMENT_TRACK_PARTITION, [self.AUDIT_TRACK])
|
|
self.verify_component_group_visibility_messsage(self.html_component, "Audit Track")
|
|
|
|
|
|
@attr(shard=16)
|
|
class UnitPublishingTest(ContainerBase):
|
|
"""
|
|
Tests of the publishing control and related widgets on the Unit page.
|
|
"""
|
|
|
|
PUBLISHED_STATUS = "Publishing Status\nPublished (not yet released)"
|
|
PUBLISHED_LIVE_STATUS = "Publishing Status\nPublished and Live"
|
|
DRAFT_STATUS = "Publishing Status\nDraft (Unpublished changes)"
|
|
LOCKED_STATUS = "Publishing Status\nVisible to Staff Only"
|
|
RELEASE_TITLE_RELEASED = "RELEASED:"
|
|
RELEASE_TITLE_RELEASE = "RELEASE:"
|
|
|
|
LAST_PUBLISHED = 'Last published'
|
|
LAST_SAVED = 'Draft saved on'
|
|
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Sets up a course structure with a unit and a single HTML child.
|
|
"""
|
|
|
|
self.html_content = '<p><strong>Body of HTML Unit.</strong></p>'
|
|
self.courseware = CoursewarePage(self.browser, self.course_id)
|
|
past_start_date = datetime.datetime(1974, 6, 22)
|
|
self.past_start_date_text = "Jun 22, 1974 at 00:00 UTC"
|
|
future_start_date = datetime.datetime(2100, 9, 13)
|
|
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
|
XBlockFixtureDesc('html', 'Test html', data=self.html_content)
|
|
)
|
|
)
|
|
),
|
|
XBlockFixtureDesc(
|
|
'chapter',
|
|
'Unlocked Section',
|
|
metadata={'start': past_start_date.isoformat()}
|
|
).add_children(
|
|
XBlockFixtureDesc('sequential', 'Unlocked Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Unlocked Unit').add_children(
|
|
XBlockFixtureDesc('problem', '<problem></problem>', data=self.html_content)
|
|
)
|
|
)
|
|
),
|
|
XBlockFixtureDesc('chapter', 'Section With Locked Unit').add_children(
|
|
XBlockFixtureDesc(
|
|
'sequential',
|
|
'Subsection With Locked Unit',
|
|
metadata={'start': past_start_date.isoformat()}
|
|
).add_children(
|
|
XBlockFixtureDesc(
|
|
'vertical',
|
|
'Locked Unit',
|
|
metadata={'visible_to_staff_only': True}
|
|
).add_children(
|
|
XBlockFixtureDesc('discussion', '', data=self.html_content)
|
|
)
|
|
)
|
|
),
|
|
XBlockFixtureDesc(
|
|
'chapter',
|
|
'Unreleased Section',
|
|
metadata={'start': future_start_date.isoformat()}
|
|
).add_children(
|
|
XBlockFixtureDesc('sequential', 'Unreleased Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Unreleased Unit')
|
|
)
|
|
)
|
|
)
|
|
|
|
def test_publishing(self):
|
|
"""
|
|
Scenario: The publish title changes based on whether or not draft content exists
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
Then the title in the Publish information box is "Published and Live"
|
|
And the Publish button is disabled
|
|
And the last published text contains "Last published"
|
|
And the last saved text contains "Last published"
|
|
And when I add a component to the unit
|
|
Then the title in the Publish information box is "Draft (Unpublished changes)"
|
|
And the last saved text contains "Draft saved on"
|
|
And the Publish button is enabled
|
|
And when I click the Publish button
|
|
Then the title in the Publish information box is "Published and Live"
|
|
And the last published text contains "Last published"
|
|
And the last saved text contains "Last published"
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
unit.verify_publish_title(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\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.
|
|
# TODO: continue discussion with Muhammad and Jay about this.
|
|
|
|
# Add a component to the page so it will have unpublished changes.
|
|
add_discussion(unit)
|
|
unit.verify_publish_title(self.DRAFT_STATUS)
|
|
self._verify_last_published_and_saved(unit, self.LAST_PUBLISHED, self.LAST_SAVED)
|
|
unit.publish()
|
|
unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
self._verify_last_published_and_saved(unit, self.LAST_PUBLISHED, self.LAST_PUBLISHED)
|
|
|
|
def test_discard_changes(self):
|
|
"""
|
|
Scenario: The publish title changes after "Discard Changes" is clicked
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
Then the Discard Changes button is disabled
|
|
And I add a component to the unit
|
|
Then the title in the Publish information box is "Draft (Unpublished changes)"
|
|
And the Discard Changes button is enabled
|
|
And when I click the Discard Changes button
|
|
Then the title in the Publish information box is "Published and Live"
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
add_discussion(unit)
|
|
unit.verify_publish_title(self.DRAFT_STATUS)
|
|
unit.discard_changes()
|
|
unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
|
|
def test_view_live_no_changes(self):
|
|
"""
|
|
Scenario: "View Live" shows published content in LMS
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
Then the View Live button is enabled
|
|
And when I click on the View Live button
|
|
Then I see the published content in LMS
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
self._view_published_version(unit)
|
|
self._verify_components_visible(['html'])
|
|
|
|
def test_view_live_changes(self):
|
|
"""
|
|
Scenario: "View Live" does not show draft content in LMS
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
And when I add a component to the unit
|
|
And when I click on the View Live button
|
|
Then I see the published content in LMS
|
|
And I do not see the unpublished component
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
add_discussion(unit)
|
|
self._view_published_version(unit)
|
|
self._verify_components_visible(['html'])
|
|
self.assertEqual(self.html_content, self.courseware.xblock_component_html_content(0))
|
|
|
|
def test_view_live_after_publish(self):
|
|
"""
|
|
Scenario: "View Live" shows newly published content
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
And when I add a component to the unit
|
|
And when I click the Publish button
|
|
And when I click on the View Live button
|
|
Then I see the newly published component
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
add_discussion(unit)
|
|
unit.publish()
|
|
self._view_published_version(unit)
|
|
self._verify_components_visible(['html', 'discussion'])
|
|
|
|
def test_initially_unlocked_visible_to_students(self):
|
|
"""
|
|
Scenario: An unlocked unit with release date in the past is visible to students
|
|
Given I have a published unlocked unit with release date in the past
|
|
When I go to the unit page in Studio
|
|
Then the unit has a warning that it is visible to students
|
|
And it is marked as "RELEASED" with release date in the past visible
|
|
And when I click on the View Live Button
|
|
And when I view the course as a student
|
|
Then I see the content in the unit
|
|
"""
|
|
unit = self.go_to_unit_page("Unlocked Section", "Unlocked Subsection", "Unlocked Unit")
|
|
unit.verify_publish_title(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 + '\n' + 'with Section "Unlocked Section"'
|
|
)
|
|
self._view_published_version(unit)
|
|
self._verify_student_view_visible(['problem'])
|
|
|
|
def test_locked_visible_to_staff_only(self):
|
|
"""
|
|
Scenario: After locking a unit with release date in the past, it is only visible to staff
|
|
Given I have a published unlocked unit with release date in the past
|
|
When I go to the unit page in Studio
|
|
And when I select "Hide from students"
|
|
Then the unit does not have a warning that it is visible to students
|
|
And 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
|
|
Then I do not see any content in the unit
|
|
"""
|
|
unit = self.go_to_unit_page("Unlocked Section", "Unlocked Subsection", "Unlocked Unit")
|
|
checked = unit.toggle_staff_lock()
|
|
self.assertTrue(checked)
|
|
self.assertFalse(unit.currently_visible_to_students)
|
|
self.assertFalse(unit.shows_inherited_staff_lock())
|
|
unit.verify_publish_title(self.LOCKED_STATUS)
|
|
self._view_published_version(unit)
|
|
# Will initially be in staff view, locked component should be visible.
|
|
self._verify_components_visible(['problem'])
|
|
# Switch to student view and verify not visible
|
|
self._verify_student_view_locked()
|
|
|
|
def test_initially_locked_not_visible_to_students(self):
|
|
"""
|
|
Scenario: A locked unit with release date in the past is not visible to students
|
|
Given I have a published locked unit with release date in the past
|
|
When I go to the unit page in Studio
|
|
Then the unit does not have a warning that it is visible to students
|
|
And it is marked as "RELEASE" with release date in the past visible
|
|
And when I click on the View Live Button
|
|
And when I view the course as a student
|
|
Then I do not see any content in the unit
|
|
"""
|
|
unit = self.go_to_unit_page("Section With Locked Unit", "Subsection With Locked Unit", "Locked Unit")
|
|
unit.verify_publish_title(self.LOCKED_STATUS)
|
|
self.assertFalse(unit.currently_visible_to_students)
|
|
self._verify_release_date_info(
|
|
unit, self.RELEASE_TITLE_RELEASE,
|
|
self.past_start_date_text + '\n' + 'with Subsection "Subsection With Locked Unit"'
|
|
)
|
|
self._view_published_version(unit)
|
|
self._verify_student_view_locked()
|
|
|
|
def test_unlocked_visible_to_all(self):
|
|
"""
|
|
Scenario: After unlocking a unit with release date in the past, it is visible to both students and staff
|
|
Given I have a published unlocked unit with release date in the past
|
|
When I go to the unit page in Studio
|
|
And when I deselect "Hide from students"
|
|
Then the unit does have a warning that it is visible to students
|
|
And when I click on the View Live Button
|
|
Then I see the content in the unit when logged in as staff
|
|
And when I view the course as a student
|
|
Then I see the content in the unit
|
|
"""
|
|
unit = self.go_to_unit_page("Section With Locked Unit", "Subsection With Locked Unit", "Locked Unit")
|
|
checked = unit.toggle_staff_lock()
|
|
self.assertFalse(checked)
|
|
unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
self.assertTrue(unit.currently_visible_to_students)
|
|
self._view_published_version(unit)
|
|
# Will initially be in staff view, components always visible.
|
|
self._verify_components_visible(['discussion'])
|
|
# Switch to student view and verify visible.
|
|
self._verify_student_view_visible(['discussion'])
|
|
|
|
def 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_cancel_does_not_create_draft(self):
|
|
"""
|
|
Scenario: Editing a component and then canceling does not create a draft version (TNL-399)
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
And edit the content of an HTML component and then press cancel
|
|
Then the content does not change
|
|
And the title in the Publish information box is "Published and Live"
|
|
And when I reload the page
|
|
Then the title in the Publish information box is "Published and Live"
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
component = unit.xblocks[1]
|
|
component.edit()
|
|
HtmlXBlockEditorView(self.browser, component.locator).set_content_and_cancel("modified content")
|
|
self.assertEqual(component.student_content, "Body of HTML Unit.")
|
|
unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
self.browser.refresh()
|
|
unit.wait_for_page()
|
|
unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
|
|
def test_delete_child_in_published_unit(self):
|
|
"""
|
|
Scenario: A published unit can be published again after deleting a child
|
|
Given I have a published unit with no unpublished changes
|
|
When I go to the unit page in Studio
|
|
And delete the only component
|
|
Then the title in the Publish information box is "Draft (Unpublished changes)"
|
|
And when I click the Publish button
|
|
Then the title in the Publish information box is "Published and Live"
|
|
And when I click the View Live button
|
|
Then I see an empty unit in LMS
|
|
"""
|
|
unit = self.go_to_unit_page()
|
|
unit.delete(0)
|
|
unit.verify_publish_title(self.DRAFT_STATUS)
|
|
unit.publish()
|
|
unit.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
self._view_published_version(unit)
|
|
self.assertEqual(0, self.courseware.num_xblock_components)
|
|
|
|
def test_published_not_live(self):
|
|
"""
|
|
Scenario: The publish title displays correctly for units that are not live
|
|
Given I have a published unit with no unpublished changes that releases in the future
|
|
When I go to the unit page in Studio
|
|
Then the title in the Publish information box is "Published (not yet released)"
|
|
And when I add a component to the unit
|
|
Then the title in the Publish information box is "Draft (Unpublished changes)"
|
|
And when I click the Publish button
|
|
Then the title in the Publish information box is "Published (not yet released)"
|
|
"""
|
|
unit = self.go_to_unit_page('Unreleased Section', 'Unreleased Subsection', 'Unreleased Unit')
|
|
unit.verify_publish_title(self.PUBLISHED_STATUS)
|
|
add_discussion(unit)
|
|
unit.verify_publish_title(self.DRAFT_STATUS)
|
|
unit.publish()
|
|
unit.verify_publish_title(self.PUBLISHED_STATUS)
|
|
|
|
def _view_published_version(self, unit):
|
|
"""
|
|
Goes to the published version, then waits for the browser to load the page.
|
|
"""
|
|
unit.view_published_version()
|
|
self.assertEqual(len(self.browser.window_handles), 2)
|
|
self.courseware.wait_for_page()
|
|
|
|
def _verify_and_return_staff_page(self):
|
|
"""
|
|
Verifies that the browser is on the staff page and returns a StaffCoursewarePage.
|
|
"""
|
|
page = StaffCoursewarePage(self.browser, self.course_id)
|
|
page.wait_for_page()
|
|
return page
|
|
|
|
def _verify_student_view_locked(self):
|
|
"""
|
|
Verifies no component is visible when viewing as a student.
|
|
"""
|
|
page = self._verify_and_return_staff_page()
|
|
page.set_staff_view_mode('Learner')
|
|
page.wait_for(lambda: self.courseware.num_xblock_components == 0, 'No XBlocks visible')
|
|
|
|
def _verify_student_view_visible(self, expected_components):
|
|
"""
|
|
Verifies expected components are visible when viewing as a student.
|
|
"""
|
|
self._verify_and_return_staff_page().set_staff_view_mode('Learner')
|
|
self._verify_components_visible(expected_components)
|
|
|
|
def _verify_components_visible(self, expected_components):
|
|
"""
|
|
Verifies the expected components are visible (and there are no extras).
|
|
"""
|
|
self.assertEqual(len(expected_components), self.courseware.num_xblock_components)
|
|
for index, component in enumerate(expected_components):
|
|
self.assertEqual(component, self.courseware.xblock_component_type(index))
|
|
|
|
def _verify_release_date_info(self, unit, expected_title, expected_date):
|
|
"""
|
|
Verifies how the release date is displayed in the publishing sidebar.
|
|
"""
|
|
self.assertEqual(expected_title, unit.release_title)
|
|
self.assertEqual(expected_date, unit.release_date)
|
|
|
|
def _verify_last_published_and_saved(self, unit, expected_published_prefix, expected_saved_prefix):
|
|
"""
|
|
Verifies that last published and last saved messages respectively contain the given strings.
|
|
"""
|
|
self.assertIn(expected_published_prefix, unit.last_published_text)
|
|
self.assertIn(expected_saved_prefix, 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()
|
|
# add_discussion(unit)
|
|
# unit.preview()
|
|
# self.assertEqual(2, self.courseware.num_xblock_components)
|
|
# self.assertEqual('html', self.courseware.xblock_component_type(0))
|
|
# self.assertEqual('discussion', self.courseware.xblock_component_type(1))
|
|
|
|
|
|
@attr(shard=3)
|
|
class DisplayNameTest(ContainerBase):
|
|
"""
|
|
Test consistent use of display_name_with_default
|
|
"""
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Sets up a course structure with nested verticals.
|
|
"""
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
|
|
XBlockFixtureDesc('vertical', None)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
def test_display_name_default(self):
|
|
"""
|
|
Scenario: Given that an XBlock with a dynamic display name has been added to the course,
|
|
When I view the unit page and note the display name of the block,
|
|
Then I see the dynamically generated display name,
|
|
And when I then go to the container page for that same block,
|
|
Then I see the same generated display name.
|
|
"""
|
|
# Unfortunately no blocks in the core platform implement display_name_with_default
|
|
# in an interesting way for this test, so we are just testing for consistency and not
|
|
# the actual value.
|
|
unit = self.go_to_unit_page()
|
|
test_block = unit.xblocks[1]
|
|
title_on_unit_page = test_block.name
|
|
container = test_block.go_to_container()
|
|
self.assertEqual(container.name, title_on_unit_page)
|
|
|
|
|
|
@attr(shard=3)
|
|
class ProblemCategoryTabsTest(ContainerBase):
|
|
"""
|
|
Test to verify tabs in problem category.
|
|
"""
|
|
def setUp(self, is_staff=True):
|
|
super(ProblemCategoryTabsTest, self).setUp(is_staff=is_staff)
|
|
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Sets up course structure.
|
|
"""
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
XBlockFixtureDesc('vertical', 'Test Unit')
|
|
)
|
|
)
|
|
)
|
|
|
|
def test_correct_tabs_present(self):
|
|
"""
|
|
Scenario: Verify that correct tabs are present in problem category.
|
|
|
|
Given I am a staff user
|
|
When I go to unit page
|
|
Then I only see `Common Problem Types` and `Advanced` tabs in `problem` category
|
|
"""
|
|
self.go_to_unit_page()
|
|
page = ContainerPage(self.browser, None)
|
|
self.assertEqual(page.get_category_tab_names('problem'), ['Common Problem Types', 'Advanced'])
|
|
|
|
def test_common_problem_types_tab(self):
|
|
"""
|
|
Scenario: Verify that correct components are present in Common Problem Types tab.
|
|
|
|
Given I am a staff user
|
|
When I go to unit page
|
|
Then I see correct components under `Common Problem Types` tab in `problem` category
|
|
"""
|
|
self.go_to_unit_page()
|
|
page = ContainerPage(self.browser, None)
|
|
|
|
expected_components = [
|
|
"Blank Common Problem",
|
|
"Checkboxes",
|
|
"Dropdown",
|
|
"Multiple Choice",
|
|
"Numerical Input",
|
|
"Text Input",
|
|
"Checkboxes with Hints and Feedback",
|
|
"Dropdown with Hints and Feedback",
|
|
"Multiple Choice with Hints and Feedback",
|
|
"Numerical Input with Hints and Feedback",
|
|
"Text Input with Hints and Feedback",
|
|
]
|
|
self.assertEqual(page.get_category_tab_components('problem', 1), expected_components)
|
|
|
|
|
|
@attr(shard=16)
|
|
@ddt.ddt
|
|
class MoveComponentTest(ContainerBase):
|
|
"""
|
|
Tests of moving an XBlock to another XBlock.
|
|
"""
|
|
PUBLISHED_LIVE_STATUS = "Publishing Status\nPublished and Live"
|
|
DRAFT_STATUS = "Publishing Status\nDraft (Unpublished changes)"
|
|
|
|
def setUp(self, is_staff=True):
|
|
super(MoveComponentTest, self).setUp(is_staff=is_staff)
|
|
self.container = ContainerPage(self.browser, None)
|
|
self.move_modal_view = MoveModalView(self.browser)
|
|
|
|
self.navigation_options = {
|
|
'section': 0,
|
|
'subsection': 0,
|
|
'unit': 1,
|
|
}
|
|
self.source_component_display_name = 'HTML 11'
|
|
self.source_xblock_category = 'component'
|
|
self.message_move = u'Success! "{display_name}" has been moved.'
|
|
self.message_undo = u'Move cancelled. "{display_name}" has been moved back to its original location.'
|
|
|
|
def populate_course_fixture(self, course_fixture):
|
|
"""
|
|
Sets up a course structure.
|
|
"""
|
|
# pylint: disable=attribute-defined-outside-init
|
|
self.unit_page1 = XBlockFixtureDesc('vertical', 'Test Unit 1').add_children(
|
|
XBlockFixtureDesc('html', 'HTML 11'),
|
|
XBlockFixtureDesc('html', 'HTML 12')
|
|
)
|
|
self.unit_page2 = XBlockFixtureDesc('vertical', 'Test Unit 2').add_children(
|
|
XBlockFixtureDesc('html', 'HTML 21'),
|
|
XBlockFixtureDesc('html', 'HTML 22')
|
|
)
|
|
course_fixture.add_children(
|
|
XBlockFixtureDesc('chapter', 'Test Section').add_children(
|
|
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
|
|
self.unit_page1,
|
|
self.unit_page2
|
|
)
|
|
)
|
|
)
|
|
|
|
def verify_move_opertions(self, unit_page, source_component, operation, component_display_names_after_operation,
|
|
should_verify_publish_title=True):
|
|
"""
|
|
Verify move operations.
|
|
|
|
Arguments:
|
|
unit_page (Object) Unit container page.
|
|
source_component (Object) Source XBlock object to be moved.
|
|
operation (str), `move` or `undo move` operation.
|
|
component_display_names_after_operation (dict) Display names of components after operation in source/dest
|
|
should_verify_publish_title (Boolean) Should verify publish title ot not. Default is True.
|
|
"""
|
|
source_component.open_move_modal()
|
|
self.move_modal_view.navigate_to_category(self.source_xblock_category, self.navigation_options)
|
|
self.assertEqual(self.move_modal_view.is_move_button_enabled, True)
|
|
|
|
# Verify unit is in published state before move operation
|
|
if should_verify_publish_title:
|
|
self.container.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
|
|
self.move_modal_view.click_move_button()
|
|
self.container.verify_confirmation_message(
|
|
self.message_move.format(display_name=self.source_component_display_name)
|
|
)
|
|
self.assertEqual(len(unit_page.displayed_children), 1)
|
|
|
|
# Verify unit in draft state now
|
|
if should_verify_publish_title:
|
|
self.container.verify_publish_title(self.DRAFT_STATUS)
|
|
|
|
if operation == 'move':
|
|
self.container.click_take_me_there_link()
|
|
elif operation == 'undo_move':
|
|
self.container.click_undo_move_link()
|
|
self.container.verify_confirmation_message(
|
|
self.message_undo.format(display_name=self.source_component_display_name)
|
|
)
|
|
|
|
unit_page = ContainerPage(self.browser, None)
|
|
components = unit_page.displayed_children
|
|
self.assertEqual(
|
|
[component.name for component in components],
|
|
component_display_names_after_operation
|
|
)
|
|
|
|
def verify_state_change(self, unit_page, operation):
|
|
"""
|
|
Verify that after state change, confirmation message is hidden.
|
|
|
|
Arguments:
|
|
unit_page (Object) Unit container page.
|
|
operation (String) Publish or discard changes operation.
|
|
"""
|
|
# Verify unit in draft state now
|
|
self.container.verify_publish_title(self.DRAFT_STATUS)
|
|
|
|
# Now click publish/discard button
|
|
if operation == 'publish':
|
|
unit_page.publish()
|
|
else:
|
|
unit_page.discard_changes()
|
|
|
|
# Now verify success message is hidden
|
|
self.container.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
self.container.verify_confirmation_message(
|
|
message=self.message_move.format(display_name=self.source_component_display_name),
|
|
verify_hidden=True
|
|
)
|
|
|
|
def test_move_component_successfully(self):
|
|
"""
|
|
Test if we can move a component successfully.
|
|
|
|
Given I am a staff user
|
|
And I go to unit page in first section
|
|
And I open the move modal
|
|
And I navigate to unit in second section
|
|
And I see move button is enabled
|
|
When I click on the move button
|
|
Then I see move operation success message
|
|
And When I click on take me there link
|
|
Then I see moved component there.
|
|
"""
|
|
unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
|
|
components = unit_page.displayed_children
|
|
self.assertEqual(len(components), 2)
|
|
|
|
self.verify_move_opertions(
|
|
unit_page=unit_page,
|
|
source_component=components[0],
|
|
operation='move',
|
|
component_display_names_after_operation=['HTML 21', 'HTML 22', 'HTML 11']
|
|
)
|
|
|
|
def test_undo_move_component_successfully(self):
|
|
"""
|
|
Test if we can undo move a component successfully.
|
|
|
|
Given I am a staff user
|
|
And I go to unit page in first section
|
|
And I open the move modal
|
|
When I click on the move button
|
|
Then I see move operation successful message
|
|
And When I clicked on undo move link
|
|
Then I see that undo move operation is successful
|
|
"""
|
|
unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
|
|
components = unit_page.displayed_children
|
|
self.assertEqual(len(components), 2)
|
|
|
|
self.verify_move_opertions(
|
|
unit_page=unit_page,
|
|
source_component=components[0],
|
|
operation='undo_move',
|
|
component_display_names_after_operation=['HTML 11', 'HTML 12']
|
|
)
|
|
|
|
@ddt.data('publish', 'discard')
|
|
def test_publish_discard_changes_afer_move(self, operation):
|
|
"""
|
|
Test if success banner is hidden when we discard changes or publish the unit after a move operation.
|
|
|
|
Given I am a staff user
|
|
And I go to unit page in first section
|
|
And I open the move modal
|
|
And I navigate to unit in second section
|
|
And I see move button is enabled
|
|
When I click on the move button
|
|
Then I see move operation success message
|
|
And When I click on publish or discard changes button
|
|
Then I see move operation success message is hidden.
|
|
"""
|
|
unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
|
|
components = unit_page.displayed_children
|
|
self.assertEqual(len(components), 2)
|
|
|
|
components[0].open_move_modal()
|
|
self.move_modal_view.navigate_to_category(self.source_xblock_category, self.navigation_options)
|
|
self.assertEqual(self.move_modal_view.is_move_button_enabled, True)
|
|
|
|
# Verify unit is in published state before move operation
|
|
self.container.verify_publish_title(self.PUBLISHED_LIVE_STATUS)
|
|
|
|
self.move_modal_view.click_move_button()
|
|
self.container.verify_confirmation_message(
|
|
self.message_move.format(display_name=self.source_component_display_name)
|
|
)
|
|
self.assertEqual(len(unit_page.displayed_children), 1)
|
|
|
|
self.verify_state_change(unit_page, operation)
|
|
|
|
def test_content_experiment(self):
|
|
"""
|
|
Test if we can move a component of content experiment successfully.
|
|
|
|
Given that I am a staff user
|
|
And I go to content experiment page
|
|
And I open the move dialogue modal
|
|
When I navigate to the unit in second section
|
|
Then I see move button is enabled
|
|
And when I click on the move button
|
|
Then I see move operation success message
|
|
And when I click on take me there link
|
|
Then I see moved component there
|
|
And when I undo move a component
|
|
Then I see that undo move operation success message
|
|
"""
|
|
# Add content experiment support to course.
|
|
self.course_fixture.add_advanced_settings({
|
|
u'advanced_modules': {'value': ['split_test']},
|
|
})
|
|
|
|
# Create group configurations
|
|
# pylint: disable=protected-access
|
|
self.course_fixture._update_xblock(self.course_fixture._course_location, {
|
|
'metadata': {
|
|
u'user_partitions': [
|
|
create_user_partition_json(
|
|
0,
|
|
'Test Group Configuration',
|
|
'Description of the group configuration.',
|
|
[Group('0', 'Group A'), Group('1', 'Group B')]
|
|
),
|
|
],
|
|
},
|
|
})
|
|
|
|
# Add split test to unit_page1 and assign newly created group configuration to it
|
|
split_test = XBlockFixtureDesc('split_test', 'Test Content Experiment', metadata={'user_partition_id': 0})
|
|
self.course_fixture.create_xblock(self.unit_page1.locator, split_test)
|
|
|
|
# Visit content experiment container page.
|
|
unit_page = ContainerPage(self.browser, split_test.locator)
|
|
unit_page.visit()
|
|
|
|
group_a_locator = unit_page.displayed_children[0].locator
|
|
|
|
# Add some components to Group A.
|
|
self.course_fixture.create_xblock(
|
|
group_a_locator, XBlockFixtureDesc('html', 'HTML 311')
|
|
)
|
|
self.course_fixture.create_xblock(
|
|
group_a_locator, XBlockFixtureDesc('html', 'HTML 312')
|
|
)
|
|
|
|
# Go to group page to move it's component.
|
|
group_container_page = ContainerPage(self.browser, group_a_locator)
|
|
group_container_page.visit()
|
|
|
|
# Verify content experiment block has correct groups and components.
|
|
components = group_container_page.displayed_children
|
|
self.assertEqual(len(components), 2)
|
|
|
|
self.source_component_display_name = 'HTML 311'
|
|
|
|
# Verify undo move operation for content experiment.
|
|
self.verify_move_opertions(
|
|
unit_page=group_container_page,
|
|
source_component=components[0],
|
|
operation='undo_move',
|
|
component_display_names_after_operation=['HTML 311', 'HTML 312'],
|
|
should_verify_publish_title=False
|
|
)
|
|
|
|
# Verify move operation for content experiment.
|
|
self.verify_move_opertions(
|
|
unit_page=group_container_page,
|
|
source_component=components[0],
|
|
operation='move',
|
|
component_display_names_after_operation=['HTML 21', 'HTML 22', 'HTML 311'],
|
|
should_verify_publish_title=False
|
|
)
|
|
|
|
# Ideally this test should be decorated with @attr('a11y') so that it should run in a11y jenkins job
|
|
# But for some reason it always fails in a11y jenkins job and passes always locally on devstack as well
|
|
# as in bokchoy jenkins job. Due to this reason, test is marked to run under bokchoy jenkins job.
|
|
def test_a11y(self):
|
|
"""
|
|
Verify move modal a11y.
|
|
"""
|
|
unit_page = self.go_to_unit_page(unit_name='Test Unit 1')
|
|
|
|
unit_page.a11y_audit.config.set_scope(
|
|
include=[".modal-window.move-modal"]
|
|
)
|
|
unit_page.a11y_audit.config.set_rules({
|
|
'ignore': [
|
|
'color-contrast', # TODO: AC-716
|
|
'link-href', # TODO: AC-716
|
|
]
|
|
})
|
|
|
|
unit_page.displayed_children[0].open_move_modal()
|
|
|
|
for category in ['section', 'subsection', 'component']:
|
|
self.move_modal_view.navigate_to_category(category, self.navigation_options)
|
|
unit_page.a11y_audit.check_for_accessibility_errors()
|