""" Tests for vertical block. """ # pylint: disable=protected-access import json from collections import namedtuple from datetime import datetime, timedelta from unittest.mock import Mock, patch import ddt import pytz from django.contrib.auth.models import AnonymousUser from django.test import override_settings from fs.memoryfs import MemoryFS from openedx_filters import PipelineStep from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted from ..x_module import AUTHOR_VIEW, PUBLIC_VIEW, STUDENT_VIEW from . import prepare_block_runtime from .helpers import StubUserService from .xml import XModuleXmlImportTest from .xml import factories as xml COMPLETION_DELAY = 9876 JsonRequest = namedtuple('JsonRequest', ['method', 'body']) def get_json_request(data): """ Given a data dictionary, return an appropriate JSON request. """ return JsonRequest( method='POST', body=json.dumps(data), ) class StubCompletionService: """ A stub implementation of the CompletionService for testing without access to django """ def __init__(self, enabled, completion_value): self._enabled = enabled self._completion_value = completion_value self.delay = COMPLETION_DELAY def completion_tracking_enabled(self): """ Turn on or off completion tracking for clients of the StubCompletionService. """ return self._enabled def get_completions(self, candidates): """ Return the (dummy) completion values for each specified candidate block. """ return {candidate: self._completion_value for candidate in candidates} def get_completable_children(self, node): return node.get_children() def get_complete_on_view_delay_ms(self): """ Return the completion-by-viewing delay in milliseconds. """ return self.delay def blocks_to_mark_complete_on_view(self, blocks): return {} if self._completion_value == 1.0 else blocks def vertical_is_complete(self, item): if item.scope_ids.block_type != 'vertical': raise ValueError('The passed in xblock is not a vertical type!') return self._completion_value == 1 if self._enabled else None class TestVerticalBlockChildRenderStep(PipelineStep): """ Utility class for testing filters on vertical block children """ filter_content = "Altered Content" def run_filter(self, block, context): # lint-amnesty, pylint: disable=arguments-differ """Pipeline step that changes child content""" if type(block).__name__ == "HtmlBlockWithMixins": block.get_html = lambda: TestVerticalBlockChildRenderStep.filter_content return {"block": block, "context": context} class TestPreventVerticalBlockChildRender(PipelineStep): """ Utility class to test vertical block children are skipped in rendering. """ def run_filter(self, block, context): # lint-amnesty, pylint: disable=arguments-differ """Pipeline step that raises exceptions during child block rendering""" if type(block).__name__ == "HtmlBlockWithMixins": raise VerticalBlockChildRenderStarted.PreventChildBlockRender( "Skip block test exception" ) class TestVerticalBlockRenderCompletedStep(PipelineStep): """ Utility class for testing filters on vertical block render completion """ filter_content = "Extra content added" def run_filter(self, block, fragment, context, view): # lint-amnesty, pylint: disable=arguments-differ """Pipeline step that alters the output of the fragment""" fragment.content += TestVerticalBlockRenderCompletedStep.filter_content return { "block": block, "fragment": fragment, "context": context, "view": view } class TestPreventVerticalBlockRenderStep(PipelineStep): """ Utility class for testing VerticalBlock render can be stopped. """ filter_content = "
Assignments are not available for Audit students.
" def run_filter(self, block, fragment, context, view): # lint-amnesty, pylint: disable=arguments-differ """Pipeline step that raises an exception""" raise VerticalBlockRenderCompleted.PreventVerticalBlockRender( TestPreventVerticalBlockRenderStep.filter_content ) class BaseVerticalBlockTest(XModuleXmlImportTest): """ Tests for the BaseVerticalBlock. """ test_html = 'Test HTML' test_problem = 'Test_Problem' test_html_nested = 'Nest Nested HTML' test_problem_nested = 'Nest_Nested_Problem' def setUp(self): super().setUp() # construct block: course/sequence/vertical - problems # \_ nested_vertical / problems course = xml.CourseFactory.build() sequence = xml.SequenceFactory.build(parent=course) vertical = xml.VerticalFactory.build(parent=sequence) self.course = self.process_xml(course) xml.HtmlFactory(parent=vertical, url_name='test-html', text=self.test_html) xml.ProblemFactory(parent=vertical, url_name='test-problem', text=self.test_problem) nested_vertical = xml.VerticalFactory.build(parent=vertical) xml.HtmlFactory(parent=nested_vertical, url_name='test_html_nested', text=self.test_html_nested) xml.ProblemFactory(parent=nested_vertical, url_name='test_problem_nested', text=self.test_problem_nested) self.course = self.process_xml(course) course_seq = self.course.get_children()[0] prepare_block_runtime(self.course.runtime) self.course.runtime.export_fs = MemoryFS() self.vertical = course_seq.get_children()[0] self.vertical.runtime = self.course.runtime self.html_block = self.vertical.get_children()[0] self.problem_block = self.vertical.get_children()[1] self.problem_block.has_score = True self.problem_block.graded = True self.extra_vertical_block = self.vertical.get_children()[2] # VerticalBlockWithMixins self.nested_problem_block = self.extra_vertical_block.get_children()[1] self.nested_problem_block.has_score = True self.nested_problem_block.graded = True self.username = "bilbo" self.default_context = {"bookmarked": False, "username": self.username} @ddt.ddt class VerticalBlockTestCase(BaseVerticalBlockTest): """ Tests for the VerticalBlock. """ def assert_bookmark_info(self, assertion, content): """ Assert content has/hasn't all the bookmark info. """ assertion('bookmark_id', content) assertion(f'{self.username},{str(self.vertical.location)}', content) assertion('bookmarked', content) assertion('show_bookmark_button', content) @ddt.unpack @ddt.data( {'context': None, 'view': STUDENT_VIEW, 'completion_value': 0.0, 'days': 1}, {'context': {}, 'view': STUDENT_VIEW, 'completion_value': 0.0, 'days': 1}, {'context': {}, 'view': PUBLIC_VIEW, 'completion_value': 0.0, 'days': 1}, {'context': {'format': 'Quiz'}, 'view': STUDENT_VIEW, 'completion_value': 1.0, 'days': 1}, # completed {'context': {'format': 'Exam'}, 'view': STUDENT_VIEW, 'completion_value': 0.0, 'days': 1}, # upcoming {'context': {'format': 'Homework'}, 'view': STUDENT_VIEW, 'completion_value': 0.0, 'days': -1}, # past due ) def test_render_student_preview_view(self, context, view, completion_value, days): """ Test the rendering of the student and public view. """ self.course.runtime._services['bookmarks'] = Mock() now = datetime.now(pytz.UTC) self.vertical.due = now + timedelta(days=days) if view == STUDENT_VIEW: self.course.runtime._services['user'] = StubUserService(user=Mock(username=self.username)) self.course.runtime._services['completion'] = StubCompletionService( enabled=True, completion_value=completion_value ) elif view == PUBLIC_VIEW: self.course.runtime._services['user'] = StubUserService(user=AnonymousUser()) html = self.course.runtime.render( self.vertical, view, self.default_context if context is None else context ).content assert self.test_html in html if view == STUDENT_VIEW: assert self.test_problem in html else: assert self.test_problem not in html assert f"'due': datetime.datetime({self.vertical.due.year}, {self.vertical.due.month}, {self.vertical.due.day}"\ in html if view == STUDENT_VIEW: self.assert_bookmark_info(self.assertIn, html) else: self.assert_bookmark_info(self.assertNotIn, html) if context: assert "'has_assignments': True" in html assert "'subsection_format': '{}'".format(context['format']) in html assert f"'completed': {completion_value == 1}" in html assert f"'past_due': {self.vertical.due < now}" in html @ddt.data(True, False) def test_render_problem_without_score(self, has_score): """ Test the rendering of the student and public view. """ self.course.runtime._services['bookmarks'] = Mock() self.course.runtime._services['user'] = StubUserService(user=Mock()) self.course.runtime._services['completion'] = StubCompletionService(enabled=True, completion_value=0) now = datetime.now(pytz.UTC) self.vertical.due = now + timedelta(days=-1) self.problem_block.has_score = has_score html = self.course.runtime.render(self.vertical, STUDENT_VIEW, self.default_context).content if has_score: assert "'has_assignments': True" in html assert "'completed': False" in html assert "'past_due': True" in html else: assert "'has_assignments': False" in html assert "'completed': None" in html assert "'past_due': False" in html @ddt.data((True, True), (True, False), (False, True), (False, False)) @ddt.unpack def test_render_access_denied_blocks(self, node_has_access_error, child_has_access_error): """ Tests access denied blocks are not rendered when hide_access_error_blocks is True """ self.course.runtime._services['bookmarks'] = Mock() self.course.runtime._services['user'] = StubUserService(user=Mock()) self.vertical.due = datetime.now(pytz.UTC) + timedelta(days=-1) self.problem_block.has_access_error = node_has_access_error self.nested_problem_block.has_access_error = child_has_access_error context = {'username': self.username, 'hide_access_error_blocks': True} html = self.course.runtime.render(self.vertical, STUDENT_VIEW, context).content if node_has_access_error and child_has_access_error: assert self.test_problem not in html assert self.test_problem_nested not in html if node_has_access_error and not child_has_access_error: assert self.test_problem not in html assert self.test_problem_nested in html if not node_has_access_error and child_has_access_error: assert self.test_problem in html assert self.test_problem_nested not in html if not node_has_access_error and not child_has_access_error: assert self.test_problem in html assert self.test_problem_nested in html @ddt.data(True, False) def test_block_has_access_error(self, has_access_error): """ Tests block_has_access_error gives the correct result for child node questions """ # Use special block from setup (vertical/nested_vertical/problem) # has_access_error is set on problem an extra level down, so we have to recurse to pass self.nested_problem_block.has_access_error = has_access_error should_block = self.vertical.block_has_access_error(self.vertical) assert should_block == has_access_error @ddt.unpack @ddt.data( (True, 0.9, True), (False, 0.9, False), (True, 1.0, False), ) def test_mark_completed_on_view_after_delay_in_context( self, completion_enabled, completion_value, mark_completed_enabled ): """ Test that mark-completed-on-view-after-delay is only set for relevant child Xblocks. """ with patch.object(self.html_block, 'render') as mock_student_view: self.course.runtime._services['completion'] = StubCompletionService( enabled=completion_enabled, completion_value=completion_value, ) self.course.runtime.render(self.vertical, STUDENT_VIEW, self.default_context) if mark_completed_enabled: assert mock_student_view.call_args[0][1]['wrap_xblock_data']['mark-completed-on-view-after-delay'] ==\ 9876 else: assert 'wrap_xblock_data' not in mock_student_view.call_args[0][1] def test_render_studio_view(self): """ Test the rendering of the Studio author view """ # Vertical shouldn't render children on the unit page context = { 'is_unit_page': True } html = self.course.runtime.render(self.vertical, AUTHOR_VIEW, context).content assert self.test_html not in html assert self.test_problem not in html # Vertical should render reorderable children on the container page reorderable_items = set() context = { 'is_unit_page': False, 'reorderable_items': reorderable_items, } html = self.course.runtime.render(self.vertical, AUTHOR_VIEW, context).content assert self.test_html in html assert self.test_problem in html @override_settings( OPEN_EDX_FILTERS_CONFIG={ "org.openedx.learning.vertical_block_child.render.started.v1": { "pipeline": [ "xmodule.tests.test_vertical.TestVerticalBlockChildRenderStep" ], "fail_silently": False, }, }, ) def test_vertical_block_child_render_started_filter_execution(self): """ Test the VerticalBlockChildRenderStarted filter's effects on student view. """ self.course.runtime._services['bookmarks'] = Mock() self.course.runtime._services['user'] = StubUserService(user=Mock()) self.course.runtime._services['completion'] = StubCompletionService(enabled=True, completion_value=0) html = self.course.runtime.render(self.vertical, STUDENT_VIEW, self.default_context).content assert TestVerticalBlockChildRenderStep.filter_content in html @override_settings( OPEN_EDX_FILTERS_CONFIG={ "org.openedx.learning.vertical_block_child.render.started.v1": { "pipeline": [ "xmodule.tests.test_vertical.TestPreventVerticalBlockChildRender" ], "fail_silently": False, }, }, ) def test_vertical_block_child_render_is_skipped_on_filter_exception(self): """ Test VerticalBlockChildRenderStarted filter can be used to skip child blocks. """ self.course.runtime._services['bookmarks'] = Mock() self.course.runtime._services['user'] = StubUserService(user=Mock()) self.course.runtime._services['completion'] = StubCompletionService(enabled=True, completion_value=0) html = self.course.runtime.render(self.vertical, STUDENT_VIEW, self.default_context).content assert self.test_html not in html assert self.test_html_nested not in html @override_settings( OPEN_EDX_FILTERS_CONFIG={ "org.openedx.learning.vertical_block.render.completed.v1": { "pipeline": [ "xmodule.tests.test_vertical.TestVerticalBlockRenderCompletedStep" ], "fail_silently": False, }, }, ) def test_vertical_block_render_completed_filter_execution(self): """ Test the VerticalBlockRenderCompleted filter's execution. """ self.course.runtime._services['bookmarks'] = Mock() self.course.runtime._services['user'] = StubUserService(user=Mock()) self.course.runtime._services['completion'] = StubCompletionService(enabled=True, completion_value=0) html = self.course.runtime.render(self.vertical, STUDENT_VIEW, self.default_context).content assert TestVerticalBlockRenderCompletedStep.filter_content in html @override_settings( OPEN_EDX_FILTERS_CONFIG={ "org.openedx.learning.vertical_block.render.completed.v1": { "pipeline": [ "xmodule.tests.test_vertical.TestPreventVerticalBlockRenderStep" ], "fail_silently": False, }, }, ) def test_vertical_block_render_output_is_changed_on_filter_exception(self): """ Test VerticalBlockRenderCompleted filter can be used to prevent vertical block from rendering. """ self.course.runtime._services['bookmarks'] = Mock() self.course.runtime._services['user'] = StubUserService(user=Mock()) self.course.runtime._services['completion'] = StubCompletionService(enabled=True, completion_value=0) html = self.course.runtime.render(self.vertical, STUDENT_VIEW, self.default_context).content assert TestPreventVerticalBlockRenderStep.filter_content == html