Problem content draft.
" } ) # Both published and draft content should be different draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True) self.assertNotEqual(draft.data, published.data) # Get problem by 'xblock_handler' view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": STUDENT_VIEW}) resp = self.client.get(view_url, HTTP_ACCEPT='application/json') self.assertEqual(resp.status_code, 200) # Activate the editing view view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": STUDIO_VIEW}) resp = self.client.get(view_url, HTTP_ACCEPT='application/json') self.assertEqual(resp.status_code, 200) # Both published and draft content should still be different draft = self.get_item_from_modulestore(self.problem_usage_key, verify_is_draft=True) self.assertNotEqual(draft.data, published.data) # Fetch the published version again to make sure the data is correct. published = modulestore().get_item(published.location, revision=ModuleStoreEnum.RevisionOption.published_only) self.assertNotEqual(draft.data, published.data) def test_publish_states_of_nested_xblocks(self): """ Test publishing of a unit page containing a nested xblock """ resp = self.create_xblock(parent_usage_key=self.seq_usage_key, display_name='Test Unit', category='vertical') unit_usage_key = self.response_usage_key(resp) resp = self.create_xblock(parent_usage_key=unit_usage_key, category='wrapper') wrapper_usage_key = self.response_usage_key(resp) resp = self.create_xblock(parent_usage_key=wrapper_usage_key, category='html') html_usage_key = self.response_usage_key(resp) # The unit and its children should be private initially unit_update_url = reverse_usage_url('xblock_handler', unit_usage_key) self.assertFalse(self._is_location_published(unit_usage_key)) self.assertFalse(self._is_location_published(html_usage_key)) # Make the unit public and verify that the problem is also made public resp = self.client.ajax_post( unit_update_url, data={'publish': 'make_public'} ) self.assertEqual(resp.status_code, 200) self._verify_published_with_no_draft(unit_usage_key) self._verify_published_with_no_draft(html_usage_key) # Make a draft for the unit and verify that the problem also has a draft resp = self.client.ajax_post( unit_update_url, data={ 'id': str(unit_usage_key), 'metadata': {}, } ) self.assertEqual(resp.status_code, 200) self._verify_published_with_draft(unit_usage_key) self._verify_published_with_draft(html_usage_key) def test_field_value_errors(self): """ Test that if the user's input causes a ValueError on an XBlock field, we provide a friendly error message back to the user. """ response = self.create_xblock(parent_usage_key=self.seq_usage_key, category='video') video_usage_key = self.response_usage_key(response) update_url = reverse_usage_url('xblock_handler', video_usage_key) response = self.client.ajax_post( update_url, data={ 'id': str(video_usage_key), 'metadata': { 'saved_video_position': "Not a valid relative time", }, } ) self.assertEqual(response.status_code, 400) parsed = json.loads(response.content.decode('utf-8')) self.assertIn("error", parsed) self.assertIn("Incorrect RelativeTime value", parsed["error"]) # See xmodule/fields.py class TestEditItemSplitMongo(TestEditItemSetup): """ Tests for EditItem running on top of the SplitMongoModuleStore. """ MODULESTORE = TEST_DATA_SPLIT_MODULESTORE def test_editing_view_wrappers(self): """ Verify that the editing view only generates a single wrapper, no matter how many times it's loaded Exposes: PLAT-417 """ view_url = reverse_usage_url("xblock_view_handler", self.problem_usage_key, {"view_name": STUDIO_VIEW}) for __ in range(3): resp = self.client.get(view_url, HTTP_ACCEPT='application/json') self.assertEqual(resp.status_code, 200) content = json.loads(resp.content.decode('utf-8')) self.assertEqual(len(PyQuery(content['html'])(f'.xblock-{STUDIO_VIEW}')), 1) class TestEditSplitModule(ItemTest): """ Tests around editing instances of the split_test module. """ def setUp(self): super().setUp() self.user = UserFactory() self.first_user_partition_group_1 = Group(str(MINIMUM_STATIC_PARTITION_ID + 1), 'alpha') self.first_user_partition_group_2 = Group(str(MINIMUM_STATIC_PARTITION_ID + 2), 'beta') self.first_user_partition = UserPartition( MINIMUM_STATIC_PARTITION_ID, 'first_partition', 'First Partition', [self.first_user_partition_group_1, self.first_user_partition_group_2] ) # There is a test point below (test_create_groups) that purposefully wants the group IDs # of the 2 partitions to overlap (which is not something that normally happens). self.second_user_partition_group_1 = Group(str(MINIMUM_STATIC_PARTITION_ID + 1), 'Group 1') self.second_user_partition_group_2 = Group(str(MINIMUM_STATIC_PARTITION_ID + 2), 'Group 2') self.second_user_partition_group_3 = Group(str(MINIMUM_STATIC_PARTITION_ID + 3), 'Group 3') self.second_user_partition = UserPartition( MINIMUM_STATIC_PARTITION_ID + 10, 'second_partition', 'Second Partition', [ self.second_user_partition_group_1, self.second_user_partition_group_2, self.second_user_partition_group_3 ] ) self.course.user_partitions = [ self.first_user_partition, self.second_user_partition ] self.store.update_item(self.course, self.user.id) root_usage_key = self._create_vertical() resp = self.create_xblock(category='split_test', parent_usage_key=root_usage_key) self.split_test_usage_key = self.response_usage_key(resp) self.split_test_update_url = reverse_usage_url("xblock_handler", self.split_test_usage_key) self.request_factory = RequestFactory() self.request = self.request_factory.get('/dummy-url') self.request.user = self.user def _update_partition_id(self, partition_id): """ Helper method that sets the user_partition_id to the supplied value. The updated split_test instance is returned. """ self.client.ajax_post( self.split_test_update_url, # Even though user_partition_id is Scope.content, it will get saved by the Studio editor as # metadata. The code in item.py will update the field correctly, even though it is not the # expected scope. data={'metadata': {'user_partition_id': str(partition_id)}} ) # Verify the partition_id was saved. split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True) self.assertEqual(partition_id, split_test.user_partition_id) return split_test def _assert_children(self, expected_number): """ Verifies the number of children of the split_test instance. """ split_test = self.get_item_from_modulestore(self.split_test_usage_key, True) self.assertEqual(expected_number, len(split_test.children)) return split_test def test_create_groups(self): """ Test that verticals are created for the configuration groups when a spit test module is edited. """ split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True) # Initially, no user_partition_id is set, and the split_test has no children. self.assertEqual(-1, split_test.user_partition_id) self.assertEqual(0, len(split_test.children)) # Set the user_partition_id to match the first user_partition. split_test = self._update_partition_id(self.first_user_partition.id) # Verify that child verticals have been set to match the groups self.assertEqual(2, len(split_test.children)) vertical_0 = self.get_item_from_modulestore(split_test.children[0], verify_is_draft=True) vertical_1 = self.get_item_from_modulestore(split_test.children[1], verify_is_draft=True) self.assertEqual("vertical", vertical_0.category) self.assertEqual("vertical", vertical_1.category) self.assertEqual("Group ID " + str(MINIMUM_STATIC_PARTITION_ID + 1), vertical_0.display_name) self.assertEqual("Group ID " + str(MINIMUM_STATIC_PARTITION_ID + 2), vertical_1.display_name) # Verify that the group_id_to_child mapping is correct. self.assertEqual(2, len(split_test.group_id_to_child)) self.assertEqual(vertical_0.location, split_test.group_id_to_child[str(self.first_user_partition_group_1.id)]) self.assertEqual(vertical_1.location, split_test.group_id_to_child[str(self.first_user_partition_group_2.id)]) def test_split_xblock_info_group_name(self): """ Test that concise outline for split test component gives display name as group name. """ split_test = self.get_item_from_modulestore(self.split_test_usage_key, verify_is_draft=True) # Initially, no user_partition_id is set, and the split_test has no children. self.assertEqual(split_test.user_partition_id, -1) self.assertEqual(len(split_test.children), 0) # Set the user_partition_id to match the first user_partition. split_test = self._update_partition_id(self.first_user_partition.id) # Verify that child verticals have been set to match the groups self.assertEqual(len(split_test.children), 2) # Get xblock outline xblock_info = create_xblock_info( split_test, is_concise=True, include_child_info=True, include_children_predicate=lambda xblock: xblock.has_children, course=self.course, user=self.request.user ) self.assertEqual(xblock_info['child_info']['children'][0]['display_name'], 'alpha') self.assertEqual(xblock_info['child_info']['children'][1]['display_name'], 'beta') def test_change_user_partition_id(self): """ Test what happens when the user_partition_id is changed to a different groups group configuration. """ # Set to first group configuration. split_test = self._update_partition_id(self.first_user_partition.id) self.assertEqual(2, len(split_test.children)) initial_vertical_0_location = split_test.children[0] initial_vertical_1_location = split_test.children[1] # Set to second group configuration split_test = self._update_partition_id(self.second_user_partition.id) # We don't remove existing children. self.assertEqual(5, len(split_test.children)) self.assertEqual(initial_vertical_0_location, split_test.children[0]) self.assertEqual(initial_vertical_1_location, split_test.children[1]) vertical_0 = self.get_item_from_modulestore(split_test.children[2], verify_is_draft=True) vertical_1 = self.get_item_from_modulestore(split_test.children[3], verify_is_draft=True) vertical_2 = self.get_item_from_modulestore(split_test.children[4], verify_is_draft=True) # Verify that the group_id_to child mapping is correct. self.assertEqual(3, len(split_test.group_id_to_child)) self.assertEqual(vertical_0.location, split_test.group_id_to_child[str(self.second_user_partition_group_1.id)]) self.assertEqual(vertical_1.location, split_test.group_id_to_child[str(self.second_user_partition_group_2.id)]) self.assertEqual(vertical_2.location, split_test.group_id_to_child[str(self.second_user_partition_group_3.id)]) self.assertNotEqual(initial_vertical_0_location, vertical_0.location) self.assertNotEqual(initial_vertical_1_location, vertical_1.location) def test_change_same_user_partition_id(self): """ Test that nothing happens when the user_partition_id is set to the same value twice. """ # Set to first group configuration. split_test = self._update_partition_id(self.first_user_partition.id) self.assertEqual(2, len(split_test.children)) initial_group_id_to_child = split_test.group_id_to_child # Set again to first group configuration. split_test = self._update_partition_id(self.first_user_partition.id) self.assertEqual(2, len(split_test.children)) self.assertEqual(initial_group_id_to_child, split_test.group_id_to_child) def test_change_non_existent_user_partition_id(self): """ Test that nothing happens when the user_partition_id is set to a value that doesn't exist. The user_partition_id will be updated, but children and group_id_to_child map will not change. """ # Set to first group configuration. split_test = self._update_partition_id(self.first_user_partition.id) self.assertEqual(2, len(split_test.children)) initial_group_id_to_child = split_test.group_id_to_child # Set to an group configuration that doesn't exist. split_test = self._update_partition_id(-50) self.assertEqual(2, len(split_test.children)) self.assertEqual(initial_group_id_to_child, split_test.group_id_to_child) def test_add_groups(self): """ Test the "fix up behavior" when groups are missing (after a group is added to a group configuration). This test actually belongs over in common, but it relies on a mutable modulestore. TODO: move tests that can go over to common after the mixed modulestore work is done. # pylint: disable=fixme """ # Set to first group configuration. split_test = self._update_partition_id(self.first_user_partition.id) # Add a group to the first group configuration. new_group_id = "1002" split_test.user_partitions = [ UserPartition( self.first_user_partition.id, 'first_partition', 'First Partition', [self.first_user_partition_group_1, self.first_user_partition_group_2, Group(new_group_id, 'pie')] ) ] self.store.update_item(split_test, self.user.id) # group_id_to_child and children have not changed yet. split_test = self._assert_children(2) group_id_to_child = split_test.group_id_to_child.copy() self.assertEqual(2, len(group_id_to_child)) # Test environment and Studio use different module systems # (CachingDescriptorSystem is used in tests, PreviewModuleSystem in Studio). # CachingDescriptorSystem doesn't have user service, that's needed for # SplitTestBlock. So, in this line of code we add this service manually. split_test.runtime._services['user'] = DjangoXBlockUserService(self.user) # pylint: disable=protected-access # Call add_missing_groups method to add the missing group. split_test.add_missing_groups(self.request) split_test = self._assert_children(3) self.assertNotEqual(group_id_to_child, split_test.group_id_to_child) group_id_to_child = split_test.group_id_to_child self.assertEqual(split_test.children[2], group_id_to_child[new_group_id]) # Call add_missing_groups again -- it should be a no-op. split_test.add_missing_groups(self.request) split_test = self._assert_children(3) self.assertEqual(group_id_to_child, split_test.group_id_to_child) @ddt.ddt class TestComponentHandler(TestCase): """Tests for component handler api""" def setUp(self): super().setUp() self.request_factory = RequestFactory() patcher = patch('cms.djangoapps.contentstore.views.component.modulestore') self.modulestore = patcher.start() self.addCleanup(patcher.stop) # component_handler calls modulestore.get_item to get the descriptor of the requested xBlock. # Here, we mock the return value of modulestore.get_item so it can be used to mock the handler # of the xBlock descriptor. self.descriptor = self.modulestore.return_value.get_item.return_value self.usage_key = BlockUsageLocator( CourseLocator('dummy_org', 'dummy_course', 'dummy_run'), 'dummy_category', 'dummy_name' ) self.usage_key_string = str(self.usage_key) self.user = UserFactory() self.request = self.request_factory.get('/dummy-url') self.request.user = self.user def test_invalid_handler(self): self.descriptor.handle.side_effect = NoSuchHandlerError with self.assertRaises(Http404): component_handler(self.request, self.usage_key_string, 'invalid_handler') @ddt.data('GET', 'POST', 'PUT', 'DELETE') def test_request_method(self, method): def check_handler(handler, request, suffix): # lint-amnesty, pylint: disable=unused-argument self.assertEqual(request.method, method) return Response() self.descriptor.handle = check_handler # Have to use the right method to create the request to get the HTTP method that we want req_factory_method = getattr(self.request_factory, method.lower()) request = req_factory_method('/dummy-url') request.user = self.user component_handler(request, self.usage_key_string, 'dummy_handler') @ddt.data(200, 404, 500) def test_response_code(self, status_code): def create_response(handler, request, suffix): # lint-amnesty, pylint: disable=unused-argument return Response(status_code=status_code) self.descriptor.handle = create_response self.assertEqual(component_handler(self.request, self.usage_key_string, 'dummy_handler').status_code, status_code) @ddt.data((True, True), (False, False),) @ddt.unpack def test_aside(self, is_xblock_aside, is_get_aside_called): """ test get_aside_from_xblock called """ def create_response(handler, request, suffix): # lint-amnesty, pylint: disable=unused-argument """create dummy response""" return Response(status_code=200) def get_usage_key(): """return usage key""" return ( str(AsideUsageKeyV2(self.usage_key, "aside")) if is_xblock_aside else self.usage_key_string ) self.descriptor.handle = create_response with patch( 'cms.djangoapps.contentstore.views.component.is_xblock_aside', return_value=is_xblock_aside ), patch( 'cms.djangoapps.contentstore.views.component.get_aside_from_xblock' ) as mocked_get_aside_from_xblock, patch( "cms.djangoapps.contentstore.views.component.webob_to_django_response" ) as mocked_webob_to_django_response: component_handler( self.request, get_usage_key(), 'dummy_handler' ) assert mocked_webob_to_django_response.called is True assert mocked_get_aside_from_xblock.called is is_get_aside_called class TestComponentTemplates(CourseTestCase): """ Unit tests for the generation of the component templates for a course. """ def setUp(self): super().setUp() # Advanced Module support levels. XBlockStudioConfiguration.objects.create(name='poll', enabled=True, support_level="fs") XBlockStudioConfiguration.objects.create(name='survey', enabled=True, support_level="ps") XBlockStudioConfiguration.objects.create(name='annotatable', enabled=True, support_level="us") # Basic component support levels. XBlockStudioConfiguration.objects.create(name='html', enabled=True, support_level="fs") XBlockStudioConfiguration.objects.create(name='discussion', enabled=True, support_level="ps") XBlockStudioConfiguration.objects.create(name='problem', enabled=True, support_level="us") XBlockStudioConfiguration.objects.create(name='video', enabled=True, support_level="us") # ORA Block has it's own category. XBlockStudioConfiguration.objects.create(name='openassessment', enabled=True, support_level="us") # XBlock masquerading as a problem XBlockStudioConfiguration.objects.create(name='drag-and-drop-v2', enabled=True, support_level="fs") XBlockStudioConfiguration.objects.create(name='staffgradedxblock', enabled=True, support_level="us") self.templates = get_component_templates(self.course) def get_templates_of_type(self, template_type): """ Returns the templates for the specified type, or None if none is found. """ template_dict = self._get_template_dict_of_type(template_type) return template_dict.get('templates') if template_dict else None def get_display_name_of_type(self, template_type): """ Returns the display name for the specified type, or None if none found. """ template_dict = self._get_template_dict_of_type(template_type) return template_dict.get('display_name') if template_dict else None def _get_template_dict_of_type(self, template_type): """ Returns a dictionary of values for a category type. """ return next((template for template in self.templates if template.get('type') == template_type), None) def get_template(self, templates, display_name): """ Returns the template which has the specified display name. """ return next((template for template in templates if template.get('display_name') == display_name), None) def test_basic_components(self): """ Test the handling of the basic component templates. """ self._verify_basic_component("discussion", "Discussion") self._verify_basic_component("video", "Video") self._verify_basic_component("openassessment", "Blank Open Response Assessment", True, 6) self._verify_basic_component_display_name("discussion", "Discussion") self._verify_basic_component_display_name("video", "Video") self._verify_basic_component_display_name("openassessment", "Open Response") self.assertGreater(len(self.get_templates_of_type('html')), 0) self.assertGreater(len(self.get_templates_of_type('problem')), 0) self.assertIsNone(self.get_templates_of_type('advanced')) # Now fully disable video through XBlockConfiguration XBlockConfiguration.objects.create(name='video', enabled=False) self.templates = get_component_templates(self.course) self.assertIsNone(self.get_templates_of_type('video')) def test_basic_components_support_levels(self): """ Test that support levels can be set on basic component templates. """ XBlockStudioConfigurationFlag.objects.create(enabled=True) self.templates = get_component_templates(self.course) self._verify_basic_component("discussion", "Discussion", "ps") self.assertEqual([], self.get_templates_of_type("video")) supported_problem_templates = [ { 'boilerplate_name': None, 'category': 'drag-and-drop-v2', 'display_name': 'Drag and Drop', 'hinted': False, 'support_level': 'fs', 'tab': 'advanced' } ] self.assertEqual(supported_problem_templates, self.get_templates_of_type("problem")) self.course.allow_unsupported_xblocks = True self.templates = get_component_templates(self.course) self._verify_basic_component("video", "Video", "us") problem_templates = self.get_templates_of_type('problem') problem_no_boilerplate = self.get_template(problem_templates, 'Blank Advanced Problem') self.assertIsNotNone(problem_no_boilerplate) self.assertEqual('us', problem_no_boilerplate['support_level']) # Now fully disable video through XBlockConfiguration XBlockConfiguration.objects.create(name='video', enabled=False) self.templates = get_component_templates(self.course) self.assertIsNone(self.get_templates_of_type('video')) def test_advanced_components(self): """ Test the handling of advanced component templates. """ self.course.advanced_modules.append('word_cloud') self.templates = get_component_templates(self.course) advanced_templates = self.get_templates_of_type('advanced') self.assertEqual(len(advanced_templates), 1) world_cloud_template = advanced_templates[0] self.assertEqual(world_cloud_template.get('category'), 'word_cloud') self.assertEqual(world_cloud_template.get('display_name'), 'Word cloud') self.assertIsNone(world_cloud_template.get('boilerplate_name', None)) # Verify that non-advanced components are not added twice self.course.advanced_modules.append('video') self.course.advanced_modules.append('drag-and-drop-v2') self.templates = get_component_templates(self.course) advanced_templates = self.get_templates_of_type('advanced') self.assertEqual(len(advanced_templates), 1) only_template = advanced_templates[0] self.assertNotEqual(only_template.get('category'), 'video') self.assertNotEqual(only_template.get('category'), 'drag-and-drop-v2') # Now fully disable word_cloud through XBlockConfiguration XBlockConfiguration.objects.create(name='word_cloud', enabled=False) self.templates = get_component_templates(self.course) self.assertIsNone(self.get_templates_of_type('advanced')) def test_advanced_problems(self): """ Test the handling of advanced problem templates. """ problem_templates = self.get_templates_of_type('problem') circuit_template = self.get_template(problem_templates, 'Circuit Schematic Builder') self.assertIsNotNone(circuit_template) self.assertEqual(circuit_template.get('category'), 'problem') self.assertEqual(circuit_template.get('boilerplate_name'), 'circuitschematic.yaml') def test_deprecated_no_advance_component_button(self): """ Test that there will be no `Advanced` button on unit page if xblocks have disabled Studio support given that they are the only modules in `Advanced Module List` """ # Update poll and survey to have "enabled=False". XBlockStudioConfiguration.objects.create(name='poll', enabled=False, support_level="fs") XBlockStudioConfiguration.objects.create(name='survey', enabled=False, support_level="fs") XBlockStudioConfigurationFlag.objects.create(enabled=True) self.course.advanced_modules.extend(['poll', 'survey']) templates = get_component_templates(self.course) button_names = [template['display_name'] for template in templates] self.assertNotIn('Advanced', button_names) def test_cannot_create_deprecated_problems(self): """ Test that xblocks that have Studio support disabled do not show on the "new component" menu. """ # Update poll to have "enabled=False". XBlockStudioConfiguration.objects.create(name='poll', enabled=False, support_level="fs") XBlockStudioConfigurationFlag.objects.create(enabled=True) self.course.advanced_modules.extend(['annotatable', 'poll', 'survey']) # Annotatable doesn't show up because it is unsupported (in test setUp). self._verify_advanced_xblocks(['Survey'], ['ps']) # Now enable unsupported components. self.course.allow_unsupported_xblocks = True self._verify_advanced_xblocks(['Annotation', 'Survey'], ['us', 'ps']) # Now disable Annotatable completely through XBlockConfiguration XBlockConfiguration.objects.create(name='annotatable', enabled=False) self._verify_advanced_xblocks(['Survey'], ['ps']) def test_create_support_level_flag_off(self): """ Test that we can create any advanced xblock (that isn't completely disabled through XBlockConfiguration) if XBlockStudioConfigurationFlag is False. """ XBlockStudioConfigurationFlag.objects.create(enabled=False) self.course.advanced_modules.extend(['annotatable', 'survey']) self._verify_advanced_xblocks(['Annotation', 'Survey'], [True, True]) def test_xblock_masquerading_as_problem(self): """ Test the integration of xblocks masquerading as problems. """ def get_xblock_problem(label): """ Helper method to get the template of any XBlock in the problems list """ self.templates = get_component_templates(self.course) problem_templates = self.get_templates_of_type('problem') return self.get_template(problem_templates, label) def verify_staffgradedxblock_present(support_level): """ Helper method to verify that staffgradedxblock template is present """ sgp = get_xblock_problem('Staff Graded Points') self.assertIsNotNone(sgp) self.assertEqual(sgp.get('category'), 'staffgradedxblock') self.assertEqual(sgp.get('support_level'), support_level) def verify_dndv2_present(support_level): """ Helper method to verify that DnDv2 template is present """ dndv2 = get_xblock_problem('Drag and Drop') self.assertIsNotNone(dndv2) self.assertEqual(dndv2.get('category'), 'drag-and-drop-v2') self.assertEqual(dndv2.get('support_level'), support_level) verify_dndv2_present(True) verify_staffgradedxblock_present(True) # Now enable XBlockStudioConfigurationFlag. The staffgradedxblock block is marked # unsupported, so will no longer show up, but DnDv2 will continue to appear. XBlockStudioConfigurationFlag.objects.create(enabled=True) self.assertIsNone(get_xblock_problem('Staff Graded Points')) self.assertIsNotNone(get_xblock_problem('Drag and Drop')) # Now allow unsupported components. self.course.allow_unsupported_xblocks = True verify_staffgradedxblock_present('us') verify_dndv2_present('fs') # Now disable the blocks completely through XBlockConfiguration XBlockConfiguration.objects.create(name='staffgradedxblock', enabled=False) XBlockConfiguration.objects.create(name='drag-and-drop-v2', enabled=False) self.assertIsNone(get_xblock_problem('Staff Graded Points')) self.assertIsNone(get_xblock_problem('Drag and Drop')) def _verify_advanced_xblocks(self, expected_xblocks, expected_support_levels): """ Verify the names of the advanced xblocks showing in the "new component" menu. """ templates = get_component_templates(self.course) button_names = [template['display_name'] for template in templates] self.assertIn('Advanced', button_names) self.assertEqual(len(templates[0]['templates']), len(expected_xblocks)) template_display_names = [template['display_name'] for template in templates[0]['templates']] self.assertEqual(template_display_names, expected_xblocks) template_support_levels = [template['support_level'] for template in templates[0]['templates']] self.assertEqual(template_support_levels, expected_support_levels) def _verify_basic_component(self, component_type, display_name, support_level=True, no_of_templates=1): """ Verify the display name and support level of basic components (that have no boilerplates). """ templates = self.get_templates_of_type(component_type) self.assertEqual(no_of_templates, len(templates)) self.assertEqual(display_name, templates[0]['display_name']) self.assertEqual(support_level, templates[0]['support_level']) def _verify_basic_component_display_name(self, component_type, display_name): """ Verify the display name of basic components. """ component_display_name = self.get_display_name_of_type(component_type) self.assertEqual(display_name, component_display_name) @ddt.ddt class TestXBlockInfo(ItemTest): """ Unit tests for XBlock's outline handling. """ def setUp(self): super().setUp() user_id = self.user.id self.chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', display_name="Week 1", user_id=user_id, highlights=['highlight'], ) self.sequential = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name="Lesson 1", user_id=user_id ) self.vertical = ItemFactory.create( parent_location=self.sequential.location, category='vertical', display_name='Unit 1', user_id=user_id ) self.video = ItemFactory.create( parent_location=self.vertical.location, category='video', display_name='My Video', user_id=user_id ) def test_json_responses(self): outline_url = reverse_usage_url('xblock_outline_handler', self.usage_key) resp = self.client.get(outline_url, HTTP_ACCEPT='application/json') json_response = json.loads(resp.content.decode('utf-8')) self.validate_course_xblock_info(json_response, course_outline=True) @ddt.data( (ModuleStoreEnum.Type.split, 4, 4), (ModuleStoreEnum.Type.mongo, 5, 7), ) @ddt.unpack def test_xblock_outline_handler_mongo_calls(self, store_type, chapter_queries, chapter_queries_1): with self.store.default_store(store_type): course = CourseFactory.create() chapter = ItemFactory.create( parent_location=course.location, category='chapter', display_name='Week 1' ) outline_url = reverse_usage_url('xblock_outline_handler', chapter.location) with check_mongo_calls(chapter_queries): self.client.get(outline_url, HTTP_ACCEPT='application/json') sequential = ItemFactory.create( parent_location=chapter.location, category='sequential', display_name='Sequential 1' ) ItemFactory.create( parent_location=sequential.location, category='vertical', display_name='Vertical 1' ) # calls should be same after adding two new children for split only. with check_mongo_calls(chapter_queries_1): self.client.get(outline_url, HTTP_ACCEPT='application/json') def test_entrance_exam_chapter_xblock_info(self): chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', display_name="Entrance Exam", user_id=self.user.id, is_entrance_exam=True ) chapter = modulestore().get_item(chapter.location) xblock_info = create_xblock_info( chapter, include_child_info=True, include_children_predicate=ALWAYS, ) # entrance exam chapter should not be deletable, draggable and childAddable. actions = xblock_info['actions'] self.assertEqual(actions['deletable'], False) self.assertEqual(actions['draggable'], False) self.assertEqual(actions['childAddable'], False) self.assertEqual(xblock_info['display_name'], 'Entrance Exam') self.assertIsNone(xblock_info.get('is_header_visible', None)) def test_none_entrance_exam_chapter_xblock_info(self): chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', display_name="Test Chapter", user_id=self.user.id ) chapter = modulestore().get_item(chapter.location) xblock_info = create_xblock_info( chapter, include_child_info=True, include_children_predicate=ALWAYS, ) # chapter should be deletable, draggable and childAddable if not an entrance exam. actions = xblock_info['actions'] self.assertEqual(actions['deletable'], True) self.assertEqual(actions['draggable'], True) self.assertEqual(actions['childAddable'], True) # chapter xblock info should not contains the key of 'is_header_visible'. self.assertIsNone(xblock_info.get('is_header_visible', None)) def test_entrance_exam_sequential_xblock_info(self): chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', display_name="Entrance Exam", user_id=self.user.id, is_entrance_exam=True, in_entrance_exam=True ) subsection = ItemFactory.create( parent_location=chapter.location, category='sequential', display_name="Subsection - Entrance Exam", user_id=self.user.id, in_entrance_exam=True ) subsection = modulestore().get_item(subsection.location) xblock_info = create_xblock_info( subsection, include_child_info=True, include_children_predicate=ALWAYS ) # in case of entrance exam subsection, header should be hidden. self.assertEqual(xblock_info['is_header_visible'], False) self.assertEqual(xblock_info['display_name'], 'Subsection - Entrance Exam') def test_none_entrance_exam_sequential_xblock_info(self): subsection = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name="Subsection - Exam", user_id=self.user.id ) subsection = modulestore().get_item(subsection.location) xblock_info = create_xblock_info( subsection, include_child_info=True, include_children_predicate=ALWAYS, parent_xblock=self.chapter ) # sequential xblock info should not contains the key of 'is_header_visible'. self.assertIsNone(xblock_info.get('is_header_visible', None)) def test_chapter_xblock_info(self): chapter = modulestore().get_item(self.chapter.location) xblock_info = create_xblock_info( chapter, include_child_info=True, include_children_predicate=ALWAYS, ) self.validate_chapter_xblock_info(xblock_info) def test_sequential_xblock_info(self): sequential = modulestore().get_item(self.sequential.location) xblock_info = create_xblock_info( sequential, include_child_info=True, include_children_predicate=ALWAYS, ) self.validate_sequential_xblock_info(xblock_info) def test_vertical_xblock_info(self): vertical = modulestore().get_item(self.vertical.location) xblock_info = create_xblock_info( vertical, include_child_info=True, include_children_predicate=ALWAYS, include_ancestor_info=True, user=self.user ) add_container_page_publishing_info(vertical, xblock_info) self.validate_vertical_xblock_info(xblock_info) def test_component_xblock_info(self): video = modulestore().get_item(self.video.location) xblock_info = create_xblock_info( video, include_child_info=True, include_children_predicate=ALWAYS ) self.validate_component_xblock_info(xblock_info) @ddt.data(ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.mongo) def test_validate_start_date(self, store_type): """ Validate if start-date year is less than 1900 reset the date to DEFAULT_START_DATE. """ with self.store.default_store(store_type): course = CourseFactory.create() chapter = ItemFactory.create( parent_location=course.location, category='chapter', display_name='Week 1' ) chapter.start = datetime(year=1899, month=1, day=1, tzinfo=UTC) xblock_info = create_xblock_info( chapter, include_child_info=True, include_children_predicate=ALWAYS, include_ancestor_info=True, user=self.user ) self.assertEqual(xblock_info['start'], DEFAULT_START_DATE.strftime('%Y-%m-%dT%H:%M:%SZ')) def test_highlights_enabled(self): self.course.highlights_enabled_for_messaging = True self.store.update_item(self.course, None) course_xblock_info = create_xblock_info(self.course) self.assertTrue(course_xblock_info['highlights_enabled_for_messaging']) def validate_course_xblock_info(self, xblock_info, has_child_info=True, course_outline=False): """ Validate that the xblock info is correct for the test course. """ self.assertEqual(xblock_info['category'], 'course') self.assertEqual(xblock_info['id'], str(self.course.location)) self.assertEqual(xblock_info['display_name'], self.course.display_name) self.assertTrue(xblock_info['published']) self.assertFalse(xblock_info['highlights_enabled_for_messaging']) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info, course_outline=course_outline) def validate_chapter_xblock_info(self, xblock_info, has_child_info=True): """ Validate that the xblock info is correct for the test chapter. """ self.assertEqual(xblock_info['category'], 'chapter') self.assertEqual(xblock_info['id'], str(self.chapter.location)) self.assertEqual(xblock_info['display_name'], 'Week 1') self.assertTrue(xblock_info['published']) self.assertIsNone(xblock_info.get('edited_by', None)) self.assertEqual(xblock_info['course_graders'], ['Homework', 'Lab', 'Midterm Exam', 'Final Exam']) self.assertEqual(xblock_info['start'], '2030-01-01T00:00:00Z') self.assertEqual(xblock_info['graded'], False) self.assertEqual(xblock_info['due'], None) self.assertEqual(xblock_info['format'], None) self.assertEqual(xblock_info['highlights'], self.chapter.highlights) self.assertTrue(xblock_info['highlights_enabled']) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) def validate_sequential_xblock_info(self, xblock_info, has_child_info=True): """ Validate that the xblock info is correct for the test sequential. """ self.assertEqual(xblock_info['category'], 'sequential') self.assertEqual(xblock_info['id'], str(self.sequential.location)) self.assertEqual(xblock_info['display_name'], 'Lesson 1') self.assertTrue(xblock_info['published']) self.assertIsNone(xblock_info.get('edited_by', None)) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=has_child_info) def validate_vertical_xblock_info(self, xblock_info): """ Validate that the xblock info is correct for the test vertical. """ self.assertEqual(xblock_info['category'], 'vertical') self.assertEqual(xblock_info['id'], str(self.vertical.location)) self.assertEqual(xblock_info['display_name'], 'Unit 1') self.assertTrue(xblock_info['published']) self.assertEqual(xblock_info['edited_by'], 'testuser') # Validate that the correct ancestor info has been included ancestor_info = xblock_info.get('ancestor_info', None) self.assertIsNotNone(ancestor_info) ancestors = ancestor_info['ancestors'] self.assertEqual(len(ancestors), 3) self.validate_sequential_xblock_info(ancestors[0], has_child_info=True) self.validate_chapter_xblock_info(ancestors[1], has_child_info=False) self.validate_course_xblock_info(ancestors[2], has_child_info=False) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info, has_child_info=True, has_ancestor_info=True) def validate_component_xblock_info(self, xblock_info): """ Validate that the xblock info is correct for the test component. """ self.assertEqual(xblock_info['category'], 'video') self.assertEqual(xblock_info['id'], str(self.video.location)) self.assertEqual(xblock_info['display_name'], 'My Video') self.assertTrue(xblock_info['published']) self.assertIsNone(xblock_info.get('edited_by', None)) # Finally, validate the entire response for consistency self.validate_xblock_info_consistency(xblock_info) def validate_xblock_info_consistency(self, xblock_info, has_ancestor_info=False, has_child_info=False, course_outline=False): """ Validate that the xblock info is internally consistent. """ self.assertIsNotNone(xblock_info['display_name']) self.assertIsNotNone(xblock_info['id']) self.assertIsNotNone(xblock_info['category']) self.assertTrue(xblock_info['published']) if has_ancestor_info: self.assertIsNotNone(xblock_info.get('ancestor_info', None)) ancestors = xblock_info['ancestor_info']['ancestors'] for ancestor in xblock_info['ancestor_info']['ancestors']: self.validate_xblock_info_consistency( ancestor, has_child_info=(ancestor == ancestors[0]), # Only the direct ancestor includes children course_outline=course_outline ) else: self.assertIsNone(xblock_info.get('ancestor_info', None)) if has_child_info: self.assertIsNotNone(xblock_info.get('child_info', None)) if xblock_info['child_info'].get('children', None): for child_response in xblock_info['child_info']['children']: self.validate_xblock_info_consistency( child_response, has_child_info=(not child_response.get('child_info', None) is None), course_outline=course_outline ) else: self.assertIsNone(xblock_info.get('child_info', None)) @patch.dict('django.conf.settings.FEATURES', {'ENABLE_SPECIAL_EXAMS': True}) class TestSpecialExamXBlockInfo(ItemTest): """ Unit tests for XBlock outline handling, specific to special exam XBlocks. """ patch_get_exam_configuration_dashboard_url = patch.object( item_module, 'get_exam_configuration_dashboard_url', return_value='test_url' ) patch_does_backend_support_onboarding = patch.object( item_module, 'does_backend_support_onboarding', return_value=True ) patch_get_exam_by_content_id_success = patch.object( item_module, 'get_exam_by_content_id' ) patch_get_exam_by_content_id_not_found = patch.object( item_module, 'get_exam_by_content_id', side_effect=ProctoredExamNotFoundException ) def setUp(self): super().setUp() user_id = self.user.id self.chapter = ItemFactory.create( parent_location=self.course.location, category='chapter', display_name="Week 1", user_id=user_id, highlights=['highlight'], ) self.course.enable_proctored_exams = True self.course.save() self.store.update_item(self.course, self.user.id) def test_proctoring_is_enabled_for_course(self): course = modulestore().get_item(self.course.location) xblock_info = create_xblock_info( course, include_child_info=True, include_children_predicate=ALWAYS, ) # exam proctoring should be enabled and time limited. assert xblock_info['enable_proctored_exams'] @patch_get_exam_configuration_dashboard_url @patch_does_backend_support_onboarding @patch_get_exam_by_content_id_success def test_special_exam_xblock_info( self, mock_get_exam_by_content_id, _mock_does_backend_support_onboarding, mock_get_exam_configuration_dashboard_url, ): sequential = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name="Test Lesson 1", user_id=self.user.id, is_proctored_exam=True, is_time_limited=True, default_time_limit_minutes=100, is_onboarding_exam=False, ) sequential = modulestore().get_item(sequential.location) xblock_info = create_xblock_info( sequential, include_child_info=True, include_children_predicate=ALWAYS, ) # exam proctoring should be enabled and time limited. assert xblock_info['is_proctored_exam'] is True assert xblock_info['was_ever_special_exam'] is True assert xblock_info['is_time_limited'] is True assert xblock_info['default_time_limit_minutes'] == 100 assert xblock_info['proctoring_exam_configuration_link'] == 'test_url' assert xblock_info['supports_onboarding'] is True assert xblock_info['is_onboarding_exam'] is False mock_get_exam_configuration_dashboard_url.assert_called_with(self.course.id, xblock_info['id']) assert mock_get_exam_by_content_id.call_count == 0 @patch_get_exam_configuration_dashboard_url @patch_does_backend_support_onboarding @patch_get_exam_by_content_id_success def test_xblock_was_ever_special_exam( self, mock_get_exam_by_content_id, _mock_does_backend_support_onboarding_patch, _mock_get_exam_configuration_dashboard_url, ): sequential = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name="Test Lesson 1", user_id=self.user.id, is_proctored_exam=False, is_time_limited=False, is_onboarding_exam=False, ) sequential = modulestore().get_item(sequential.location) xblock_info = create_xblock_info( sequential, include_child_info=True, include_children_predicate=ALWAYS, ) assert xblock_info['was_ever_special_exam'] is True assert mock_get_exam_by_content_id.call_count == 1 @patch_get_exam_configuration_dashboard_url @patch_does_backend_support_onboarding @patch_get_exam_by_content_id_not_found def test_xblock_was_never_proctored_exam( self, mock_get_exam_by_content_id, _mock_does_backend_support_onboarding_patch, _mock_get_exam_configuration_dashboard_url, ): sequential = ItemFactory.create( parent_location=self.chapter.location, category='sequential', display_name="Test Lesson 1", user_id=self.user.id, is_proctored_exam=False, is_time_limited=False, is_onboarding_exam=False, ) sequential = modulestore().get_item(sequential.location) xblock_info = create_xblock_info( sequential, include_child_info=True, include_children_predicate=ALWAYS, ) assert xblock_info['was_ever_special_exam'] is False assert mock_get_exam_by_content_id.call_count == 1 class TestLibraryXBlockInfo(ModuleStoreTestCase): """ Unit tests for XBlock Info for XBlocks in a content library """ def setUp(self): super().setUp() user_id = self.user.id self.library = LibraryFactory.create() self.top_level_html = ItemFactory.create( parent_location=self.library.location, category='html', user_id=user_id, publish_item=False ) self.vertical = ItemFactory.create( parent_location=self.library.location, category='vertical', user_id=user_id, publish_item=False ) self.child_html = ItemFactory.create( parent_location=self.vertical.location, category='html', display_name='Test HTML Child Block', user_id=user_id, publish_item=False ) def test_lib_xblock_info(self): html_block = modulestore().get_item(self.top_level_html.location) xblock_info = create_xblock_info(html_block) self.validate_component_xblock_info(xblock_info, html_block) self.assertIsNone(xblock_info.get('child_info', None)) def test_lib_child_xblock_info(self): html_block = modulestore().get_item(self.child_html.location) xblock_info = create_xblock_info(html_block, include_ancestor_info=True, include_child_info=True) self.validate_component_xblock_info(xblock_info, html_block) self.assertIsNone(xblock_info.get('child_info', None)) ancestors = xblock_info['ancestor_info']['ancestors'] self.assertEqual(len(ancestors), 2) self.assertEqual(ancestors[0]['category'], 'vertical') self.assertEqual(ancestors[0]['id'], str(self.vertical.location)) self.assertEqual(ancestors[1]['category'], 'library') def validate_component_xblock_info(self, xblock_info, original_block): """ Validate that the xblock info is correct for the test component. """ self.assertEqual(xblock_info['category'], original_block.category) self.assertEqual(xblock_info['id'], str(original_block.location)) self.assertEqual(xblock_info['display_name'], original_block.display_name) self.assertIsNone(xblock_info.get('has_changes', None)) self.assertIsNone(xblock_info.get('published', None)) self.assertIsNone(xblock_info.get('published_on', None)) self.assertIsNone(xblock_info.get('graders', None)) class TestLibraryXBlockCreation(ItemTest): """ Tests the adding of XBlocks to Library """ def test_add_xblock(self): """ Verify we can add an XBlock to a Library. """ lib = LibraryFactory.create() self.create_xblock(parent_usage_key=lib.location, display_name='Test', category="html") lib = self.store.get_library(lib.location.library_key) self.assertTrue(lib.children) xblock_locator = lib.children[0] self.assertEqual(self.store.get_item(xblock_locator).display_name, 'Test') def test_no_add_discussion(self): """ Verify we cannot add a discussion module to a Library. """ lib = LibraryFactory.create() response = self.create_xblock(parent_usage_key=lib.location, display_name='Test', category='discussion') self.assertEqual(response.status_code, 400) lib = self.store.get_library(lib.location.library_key) self.assertFalse(lib.children) def test_no_add_advanced(self): lib = LibraryFactory.create() lib.advanced_modules = ['lti'] lib.save() response = self.create_xblock(parent_usage_key=lib.location, display_name='Test', category='lti') self.assertEqual(response.status_code, 400) lib = self.store.get_library(lib.location.library_key) self.assertFalse(lib.children) @ddt.ddt class TestXBlockPublishingInfo(ItemTest): """ Unit tests for XBlock's outline handling. """ FIRST_SUBSECTION_PATH = [0] FIRST_UNIT_PATH = [0, 0] SECOND_UNIT_PATH = [0, 1] def _create_child(self, parent, category, display_name, publish_item=False, staff_only=False): """ Creates a child xblock for the given parent. """ child = ItemFactory.create( parent_location=parent.location, category=category, display_name=display_name, user_id=self.user.id, publish_item=publish_item ) if staff_only: self._enable_staff_only(child.location) # In case the staff_only state was set, return the updated xblock. return modulestore().get_item(child.location) def _get_child_xblock_info(self, xblock_info, index): """ Returns the child xblock info at the specified index. """ children = xblock_info['child_info']['children'] self.assertGreater(len(children), index) return children[index] def _get_xblock_info(self, location): """ Returns the xblock info for the specified location. """ return create_xblock_info( modulestore().get_item(location), include_child_info=True, 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. """ xblock = modulestore().get_item(location) xblock.start = start self.store.update_item(xblock, self.user.id) def _enable_staff_only(self, location): """ Enables staff only for the specified xblock. """ xblock = modulestore().get_item(location) xblock.visible_to_staff_only = True self.store.update_item(xblock, self.user.id) def _set_display_name(self, location, display_name): """ Sets the display name for the specified xblock. """ xblock = modulestore().get_item(location) xblock.display_name = display_name self.store.update_item(xblock, self.user.id) def _verify_xblock_info_state(self, xblock_info, xblock_info_field, expected_state, path=None, should_equal=True): """ 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_xblock_info_state(direct_child_xblock_info, xblock_info_field, expected_state, remaining_path, should_equal) else: 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 test_empty_chapter(self): empty_chapter = self._create_child(self.course, 'chapter', "Empty Chapter") xblock_info = self._get_xblock_info(empty_chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.unscheduled) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_chapter_self_paced_default_start_date(self, store_type): course = CourseFactory.create(default_store=store_type) course.self_paced = True self.store.update_item(course, self.user.id) chapter = self._create_child(course, 'chapter', "Test Chapter") sequential = self._create_child(chapter, 'sequential', "Test Sequential") self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) self._set_release_date(chapter.location, DEFAULT_START_DATE) xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.live) def test_empty_sequential(self): chapter = self._create_child(self.course, 'chapter', "Test Chapter") self._create_child(chapter, 'sequential', "Empty Sequential") xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.unscheduled) self._verify_visibility_state(xblock_info, VisibilityState.unscheduled, path=self.FIRST_SUBSECTION_PATH) def test_published_unit(self): """ Tests the visibility state of a published unit with release date in the future. """ chapter = self._create_child(self.course, 'chapter', "Test Chapter") sequential = self._create_child(chapter, 'sequential', "Test Sequential") self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1)) xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.ready) self._verify_visibility_state(xblock_info, VisibilityState.ready, path=self.FIRST_SUBSECTION_PATH) self._verify_visibility_state(xblock_info, VisibilityState.ready, path=self.FIRST_UNIT_PATH) self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) def test_released_unit(self): """ Tests the visibility state of a published unit with release date in the past. """ chapter = self._create_child(self.course, 'chapter', "Test Chapter") sequential = self._create_child(chapter, 'sequential', "Test Sequential") self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1)) xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.live) 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_unpublished_changes(self): """ Tests the visibility state of a published unit with draft (unpublished) changes. """ chapter = self._create_child(self.course, 'chapter', "Test Chapter") sequential = self._create_child(chapter, 'sequential', "Test Sequential") unit = self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) # Setting the display name creates a draft version of unit. self._set_display_name(unit.location, 'Updated Unit') xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.needs_attention) self._verify_visibility_state(xblock_info, VisibilityState.needs_attention, path=self.FIRST_SUBSECTION_PATH) self._verify_visibility_state(xblock_info, VisibilityState.needs_attention, path=self.FIRST_UNIT_PATH) self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=self.SECOND_UNIT_PATH) def test_partially_released_section(self): chapter = self._create_child(self.course, 'chapter', "Test Chapter") released_sequential = self._create_child(chapter, 'sequential', "Released Sequential") self._create_child(released_sequential, 'vertical', "Released Unit", publish_item=True) self._create_child(released_sequential, 'vertical', "Staff Only Unit", staff_only=True) self._set_release_date(chapter.location, datetime.now(UTC) - timedelta(days=1)) published_sequential = self._create_child(chapter, 'sequential', "Published Sequential") self._create_child(published_sequential, 'vertical', "Published Unit", publish_item=True) self._create_child(published_sequential, 'vertical', "Staff Only Unit", staff_only=True) self._set_release_date(published_sequential.location, datetime.now(UTC) + timedelta(days=1)) xblock_info = self._get_xblock_info(chapter.location) # Verify the state of the released sequential self._verify_visibility_state(xblock_info, VisibilityState.live, path=[0]) self._verify_visibility_state(xblock_info, VisibilityState.live, path=[0, 0]) self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[0, 1]) # Verify the state of the published sequential self._verify_visibility_state(xblock_info, VisibilityState.ready, path=[1]) self._verify_visibility_state(xblock_info, VisibilityState.ready, path=[1, 0]) self._verify_visibility_state(xblock_info, VisibilityState.staff_only, path=[1, 1]) # Finally verify the state of the chapter self._verify_visibility_state(xblock_info, VisibilityState.ready) 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") vertical = 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) vertical_info = self._get_xblock_info(vertical.location) add_container_page_publishing_info(vertical, vertical_info) self.assertEqual(_xblock_type_and_display_name(chapter), vertical_info["staff_lock_from"]) 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) vertical = 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) vertical_info = self._get_xblock_info(vertical.location) add_container_page_publishing_info(vertical, vertical_info) self.assertEqual(_xblock_type_and_display_name(sequential), vertical_info["staff_lock_from"]) 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") vertical = 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) vertical_info = self._get_xblock_info(vertical.location) add_container_page_publishing_info(vertical, vertical_info) self.assertEqual(_xblock_type_and_display_name(vertical), vertical_info["staff_lock_from"]) 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") self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) self._set_release_date(sequential.location, datetime.now(UTC) - timedelta(days=1)) xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.needs_attention) 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_unreleased_section_with_live_subsection(self): chapter = self._create_child(self.course, 'chapter', "Test Chapter") sequential = self._create_child(chapter, 'sequential', "Test Sequential") self._create_child(sequential, 'vertical', "Published Unit", publish_item=True) self._create_child(sequential, 'vertical', "Staff Only Unit", staff_only=True) self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1)) self._set_release_date(sequential.location, datetime.now(UTC) - timedelta(days=1)) xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.needs_attention) 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) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) def test_self_paced_item_visibility_state(self, store_type): """ Test that in self-paced course, item has `live` visibility state. Test that when item was initially in `scheduled` state in instructor mode, change course pacing to self-paced, now in self-paced course, item should have `live` visibility state. """ # Create course, chapter and setup future release date to make chapter in scheduled state course = CourseFactory.create(default_store=store_type) chapter = self._create_child(course, 'chapter', "Test Chapter") self._set_release_date(chapter.location, datetime.now(UTC) + timedelta(days=1)) # Check that chapter has scheduled state xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.ready) self.assertFalse(course.self_paced) # Change course pacing to self paced course.self_paced = True self.store.update_item(course, self.user.id) self.assertTrue(course.self_paced) # Check that in self paced course content has live state now xblock_info = self._get_xblock_info(chapter.location) self._verify_visibility_state(xblock_info, VisibilityState.live)