From 2b5926524046b3d08b255a736ac0a6e758e4262d Mon Sep 17 00:00:00 2001 From: Arunmozhi Date: Thu, 23 Feb 2023 00:35:19 +0530 Subject: [PATCH] feat: adds VerticalBlockRenderCompleted filter hook (#31388) * feat: adds VerticalBlockChildrenLoaded filter call This introduces the VerticalBlockChildrenLoaded filter that is run after all the child blocks are fetched before rendering a student or the public view. This will allow modifying the contents of the VerticalBlock before presenting it to the students. --- docs/guides/hooks/filters.rst | 10 ++- xmodule/tests/test_vertical.py | 148 +++++++++++++++++++++++++++++++++ xmodule/vertical_block.py | 28 +++++-- 3 files changed, 176 insertions(+), 10 deletions(-) diff --git a/docs/guides/hooks/filters.rst b/docs/guides/hooks/filters.rst index 9e8496aead..0eb1527503 100644 --- a/docs/guides/hooks/filters.rst +++ b/docs/guides/hooks/filters.rst @@ -182,6 +182,10 @@ well as the trigger location in this same repository. - org.openedx.learning.dashboard.render.started.v1 - `2022-06-14 `_ - * - `VerticalChildRenderStarted `_ - - org.openedx.learning.veritical_child_block.render.started.v1 - - `2022-08-18 `_ + * - `VerticalBlockChildRenderStarted `_ + - org.openedx.learning.veritical_block_child.render.started.v1 + - `2022-08-18 `_ + + * - `VerticalBlockRenderCompleted `_ + - org.openedx.learning.veritical_block.render.completed.v1 + - `2022-02-18 `_ diff --git a/xmodule/tests/test_vertical.py b/xmodule/tests/test_vertical.py index a7f993808f..1717354b47 100644 --- a/xmodule/tests/test_vertical.py +++ b/xmodule/tests/test_vertical.py @@ -14,6 +14,9 @@ import pytz import ddt from fs.memoryfs import MemoryFS from django.contrib.auth.models import AnonymousUser +from django.test import override_settings +from openedx_filters import PipelineStep +from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted from . import get_test_system from .helpers import StubUserService @@ -78,6 +81,62 @@ class StubCompletionService: 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. @@ -289,3 +348,92 @@ class VerticalBlockTestCase(BaseVerticalBlockTest): html = self.module_system.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.module_system._services['bookmarks'] = Mock() + self.module_system._services['user'] = StubUserService(user=Mock()) + self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0) + + html = self.module_system.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.module_system._services['bookmarks'] = Mock() + self.module_system._services['user'] = StubUserService(user=Mock()) + self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0) + + html = self.module_system.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.module_system._services['bookmarks'] = Mock() + self.module_system._services['user'] = StubUserService(user=Mock()) + self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0) + + html = self.module_system.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.module_system._services['bookmarks'] = Mock() + self.module_system._services['user'] = StubUserService(user=Mock()) + self.module_system._services['completion'] = StubCompletionService(enabled=True, completion_value=0) + + html = self.module_system.render(self.vertical, STUDENT_VIEW, self.default_context).content + + assert TestPreventVerticalBlockRenderStep.filter_content == html diff --git a/xmodule/vertical_block.py b/xmodule/vertical_block.py index 6d6f8aef8a..f66e902036 100644 --- a/xmodule/vertical_block.py +++ b/xmodule/vertical_block.py @@ -13,7 +13,7 @@ from lxml import etree from web_fragments.fragment import Fragment from xblock.core import XBlock # lint-amnesty, pylint: disable=wrong-import-order from xblock.fields import Boolean, Scope -from openedx_filters.learning.filters import VerticalBlockChildRenderStarted +from openedx_filters.learning.filters import VerticalBlockChildRenderStarted, VerticalBlockRenderCompleted from xmodule.mako_block import MakoTemplateBlockBase from xmodule.progress import Progress from xmodule.seq_block import SequenceFields @@ -72,7 +72,7 @@ class VerticalBlock( show_in_read_only_mode = True - def _student_or_public_view(self, context, view): + def _student_or_public_view(self, context, view): # lint-amnesty, pylint: disable=too-many-statements """ Renders the requested view type of the block in the LMS. """ @@ -117,11 +117,15 @@ class VerticalBlock( 'mark-completed-on-view-after-delay': complete_on_view_delay } - # .. filter_implemented_name: VerticalBlockChildRenderStarted - # .. filter_type: org.openedx.learning.vertical_block_child.render.started.v1 - child, child_block_context = VerticalBlockChildRenderStarted.run_filter( - block=child, context=child_block_context - ) + try: + # .. filter_implemented_name: VerticalBlockChildRenderStarted + # .. filter_type: org.openedx.learning.vertical_block_child.render.started.v1 + child, child_block_context = VerticalBlockChildRenderStarted.run_filter( + block=child, context=child_block_context + ) + except VerticalBlockChildRenderStarted.PreventChildBlockRender as exc: + log.info("Skipping %s from vertical block. Reason: %s", child, exc.message) + continue rendered_child = child.render(view, child_block_context) fragment.add_fragment_resources(rendered_child) @@ -162,6 +166,16 @@ class VerticalBlock( add_webpack_to_fragment(fragment, 'VerticalStudentView') fragment.initialize_js('VerticalStudentView') + try: + # .. filter_implemented_name: VerticalBlockRenderCompleted + # .. filter_type: org.openedx.learning.vertical_block.render.completed.v1 + _, fragment, context, view = VerticalBlockRenderCompleted.run_filter( + block=self, fragment=fragment, context=context, view=view + ) + except VerticalBlockRenderCompleted.PreventVerticalBlockRender as exc: + log.info("VerticalBlock rendering stopped. Reason: %s", exc.message) + fragment.content = exc.message + return fragment def block_has_access_error(self, block):