From 64626a77c9317c8f1d4d744a5a068e8e35a8ce33 Mon Sep 17 00:00:00 2001 From: Irfan Ahmad Date: Thu, 11 Jun 2026 17:15:41 +0500 Subject: [PATCH 1/4] feat: remove built-in HTML XBlock implementation (slash-n-burn #37819) Removes the _BuiltinHtmlBlockMixin, _BuiltInHtmlBlock classes, the HtmlBlockMixin/HtmlBlock shims, and the USE_EXTRACTED_HTML_BLOCK toggle now that the extracted version in xblocks-contrib is the only implementation. AboutBlock, StaticTabBlock, and CourseInfoBlock remain in html_block.py and now inherit from xblocks_contrib.html.HtmlBlockMixin directly. HtmlBlockDisplay webpack entry removed; HtmlBlockEditor kept since the instructor dashboard loads it explicitly. Co-Authored-By: Claude Sonnet 4.6 --- .../courseware/tests/test_block_render.py | 3 +- openedx/envs/common.py | 8 - pyproject.toml | 1 - webpack.builtinblocks.config.js | 8 - xmodule/html_block.py | 415 +------------- .../css-builtin-blocks/HtmlBlockDisplay.css | 507 ------------------ xmodule/tests/test_html_block.py | 269 +--------- 7 files changed, 5 insertions(+), 1206 deletions(-) delete mode 100644 xmodule/static/css-builtin-blocks/HtmlBlockDisplay.css diff --git a/lms/djangoapps/courseware/tests/test_block_render.py b/lms/djangoapps/courseware/tests/test_block_render.py index 2744592610ec..ae1c9ebb4ec6 100644 --- a/lms/djangoapps/courseware/tests/test_block_render.py +++ b/lms/djangoapps/courseware/tests/test_block_render.py @@ -96,7 +96,8 @@ from openedx.core.lib.url_utils import quote_slashes from xmodule.capa_block import ProblemBlock from xmodule.contentstore.django import contentstore -from xmodule.html_block import AboutBlock, CourseInfoBlock, HtmlBlock, StaticTabBlock +from xblocks_contrib.html import HtmlBlock +from xmodule.html_block import AboutBlock, CourseInfoBlock, StaticTabBlock from xmodule.lti_block import LTIBlock from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import XBlockI18nService, modulestore diff --git a/openedx/envs/common.py b/openedx/envs/common.py index 31b0015d34d9..d6e5fe3dabec 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -2095,14 +2095,6 @@ def add_optional_apps(optional_apps, installed_apps): # .. toggle_target_removal_date: 2026-04-10 USE_EXTRACTED_LTI_BLOCK = True -# .. toggle_name: USE_EXTRACTED_HTML_BLOCK -# .. toggle_default: True -# .. toggle_implementation: DjangoSetting -# .. toggle_description: Enables the use of the extracted HTML XBlock, which has been shifted to the 'openedx/xblocks-contrib' repo. -# .. toggle_use_cases: temporary -# .. toggle_creation_date: 2024-11-10 -# .. toggle_target_removal_date: 2026-04-10 -USE_EXTRACTED_HTML_BLOCK = True # .. toggle_name: USE_EXTRACTED_DISCUSSION_BLOCK # .. toggle_default: False diff --git a/pyproject.toml b/pyproject.toml index 44488938ec95..108b2a9c8eee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ discuss = "xmodule.template_block:TranslateCustomTagBlock" discussion = "xmodule.discussion_block:DiscussionXBlock" error = "xmodule.error_block:ErrorBlock" hidden = "xmodule.hidden_block:HiddenBlock" -html = "xmodule.html_block:HtmlBlock" itembank = "xmodule.item_bank_block:ItemBankBlock" image = "xmodule.template_block:TranslateCustomTagBlock" library = "xmodule.library_root_xblock:LibraryRoot" diff --git a/webpack.builtinblocks.config.js b/webpack.builtinblocks.config.js index 20d8790fc5b5..0445d795df16 100644 --- a/webpack.builtinblocks.config.js +++ b/webpack.builtinblocks.config.js @@ -26,14 +26,6 @@ module.exports = { './xmodule/js/src/xmodule.js', './xmodule/js/src/raw/edit/xml.js' ], - HtmlBlockDisplay: [ - './xmodule/js/src/xmodule.js', - './xmodule/js/src/html/display.js', - './xmodule/js/src/javascript_loader.js', - './xmodule/js/src/collapsible.js', - './xmodule/js/src/html/imageModal.js', - './xmodule/js/common_static/js/vendor/draggabilly.js' - ], HtmlBlockEditor: [ './xmodule/js/src/xmodule.js', './xmodule/js/src/html/edit.js' diff --git a/xmodule/html_block.py b/xmodule/html_block.py index 0532aef3964d..1f4120d24183 100644 --- a/xmodule/html_block.py +++ b/xmodule/html_block.py @@ -1,403 +1,19 @@ # pylint: disable=missing-module-docstring -import copy -import logging -import os -import re -import sys import textwrap -import warnings from datetime import datetime -from django.conf import settings -from fs.errors import ResourceNotFound -from lxml import etree -from path import Path as path -from web_fragments.fragment import Fragment from xblock.core import XBlock from xblock.fields import Boolean, List, Scope, String -from xblocks_contrib.html import HtmlBlock as _ExtractedHtmlBlock -from xblocks_contrib.html import HtmlBlockMixin as _ExtractedHtmlBlockMixin +from xblocks_contrib.html import HtmlBlockMixin -from common.djangoapps.xblock_django.constants import ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID -from xmodule.contentstore.content import StaticContent -from xmodule.editing_block import EditingMixin -from xmodule.edxnotes_utils import edxnotes -from xmodule.html_checker import check_html -from xmodule.stringify import stringify_children -from xmodule.util.builtin_assets import add_css_to_fragment, add_webpack_js_to_fragment -from xmodule.util.misc import escape_html_characters -from xmodule.x_module import ResourceTemplates, XModuleMixin, XModuleToXBlockMixin, shim_xmodule_js -from xmodule.xml_block import XmlMixin, name_to_pathname - -log = logging.getLogger("edx.courseware") +from xmodule.x_module import ResourceTemplates # Make '_' a no-op so we can scrape strings. Using lambda instead of # `django.utils.translation.ugettext_noop` because Django cannot be imported in this file _ = lambda text: text -@XBlock.needs("i18n") -@XBlock.needs("mako") -@XBlock.needs("user") -class _BuiltinHtmlBlockMixin( # pylint: disable=abstract-method - XmlMixin, EditingMixin, - XModuleToXBlockMixin, XModuleMixin, -): - """ - The HTML XBlock mixin. - This provides the base class for all Html-ish blocks (including the HTML XBlock). - - .. deprecated:: 2026-03 - This built-in HTML block mixin is deprecated. Please use the extracted ``HtmlBlockMixin`` - from ``xblocks_contrib.html`` instead. - """ - - display_name = String( - display_name=_("Display Name"), - help=_("The display name for this component."), - scope=Scope.settings, - # it'd be nice to have a useful default but it screws up other things; so, - # use display_name_with_default for those - default=_("Text") - ) - data = String(help=_("Html contents to display for this block"), default="", scope=Scope.content) - upstream_data = String( - help=_("Upstream html contents to store upstream data field"), - default=None, - hidden=True, - enforce_type=True, - scope=Scope.content, - ) - source_code = String( - help=_("Source code for LaTeX documents. This feature is not well-supported."), - scope=Scope.settings - ) - use_latex_compiler = Boolean( - help=_("Enable LaTeX templates?"), - default=False, - scope=Scope.settings - ) - editor = String( - help=_( - "Select Visual to enter content and have the editor automatically create the HTML. Select Raw to edit " - "HTML directly. If you change this setting, you must save the component and then re-open it for editing." - ), - display_name=_("Editor"), - default="visual", - values=[ - {"display_name": _("Visual"), "value": "visual"}, - {"display_name": _("Raw"), "value": "raw"} - ], - scope=Scope.settings - ) - - ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA = 'ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA' - - @XBlock.supports("multi_device") - def student_view(self, _context): - """ - Return a fragment that contains the html for the student view - """ - fragment = Fragment(self.get_html()) - add_css_to_fragment(fragment, 'HtmlBlockDisplay.css') - add_webpack_js_to_fragment(fragment, 'HtmlBlockDisplay') - shim_xmodule_js(fragment, 'HTMLModule') - return fragment - - @XBlock.supports("multi_device") - def public_view(self, context): - """ - Returns a fragment that contains the html for the preview view - """ - return self.student_view(context) - - def student_view_data(self, context=None): # pylint: disable=unused-argument - """ - Return a JSON representation of the student_view of this XBlock. - """ - if getattr(settings, 'FEATURES', {}).get(self.ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA, False): - return {'enabled': True, 'html': self.get_html()} - else: - return { - 'enabled': False, - 'message': f'To enable, set FEATURES["{self.ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA}"]' - } - - def get_html(self): - """ Returns html required for rendering the block. """ - if self.data: - data = self.data - user = ( - self.runtime.service(self, 'user') - .get_current_user() - ) - user_id = user.opt_attrs.get(ATTR_KEY_DEPRECATED_ANONYMOUS_USER_ID) - if user_id: - data = data.replace("%%USER_ID%%", user_id) - data = data.replace("%%COURSE_ID%%", str(self.scope_ids.usage_id.context_key)) - - if user.emails: - email = user.emails[0] - data = data.replace("%%USER_EMAIL%%", email) - - return data - return self.data - - def studio_view(self, _context): - """ - Return the studio view. - """ - # Only the ReactJS editor is supported for this block. - # See https://github.com/openedx/frontend-app-authoring/tree/master/src/editors/containers/TextEditor - raise NotImplementedError - - @classmethod - def get_customizable_fields(cls) -> dict[str, str | None]: - return { - "display_name": "upstream_display_name", - "data": "upstream_data", - } - - uses_xmodule_styles_setup = True - - mako_template = "widgets/html-edit.html" - resources_dir = None - filename_extension = "xml" - template_dir_name = "html" - show_in_read_only_mode = True - - # VS[compat] TODO (cpennington): Delete this method once all fall 2012 course - # are being edited in the cms - @classmethod - def backcompat_paths(cls, filepath): - """ - Get paths for html and xml files. - """ - if filepath.endswith('.html.xml'): - filepath = filepath[:-9] + '.html' # backcompat--look for html instead of xml - if filepath.endswith('.html.html'): - filepath = filepath[:-5] # some people like to include .html in filenames.. - candidates = [] - while os.sep in filepath: - candidates.append(filepath) - _, _, filepath = filepath.partition(os.sep) - - # also look for .html versions instead of .xml - new_candidates = [] - for candidate in candidates: - if candidate.endswith('.xml'): - new_candidates.append(candidate[:-4] + '.html') - return candidates + new_candidates - - @classmethod - def filter_templates(cls, template, course): - """ - Filter template that contains 'latex' from templates. - - Show them only if use_latex_compiler is set to True in - course settings. - """ - return 'latex' not in template['template_id'] or course.use_latex_compiler - - def get_context(self): - """ - an override to add in specific rendering context, in this case we need to - add in a base path to our c4x content addressing scheme - """ - _context = EditingMixin.get_context(self) - # Add some specific HTML rendering context when editing HTML blocks where we pass - # the root /c4x/ url for assets. This allows client-side substitutions to occur. - _context.update({ - 'base_asset_url': StaticContent.get_base_url_path_for_course_assets(self.location.course_key), - 'enable_latex_compiler': self.use_latex_compiler, - 'editor': self.editor - }) - return _context - - # NOTE: html descriptors are special. We do not want to parse and - # export them ourselves, because that can break things (e.g. lxml - # adds body tags when it exports, but they should just be html - # snippets that will be included in the middle of pages. - - @classmethod - def load_definition(cls, xml_object, system, location, id_generator): # pylint: disable=arguments-differ - '''Load a descriptor from the specified xml_object: - - If there is a filename attribute, load it as a string, and - log a warning if it is not parseable by etree.HTMLParser. - - If there is not a filename attribute, the definition is the body - of the xml_object, without the root tag (do not want in the - middle of a page) - - Args: - xml_object: an lxml.etree._Element containing the definition to load - system: the modulestore system or runtime which caches data - location: the usage id for the block--used to compute the filename if none in the xml_object - id_generator: used by other impls of this method to generate the usage_id - ''' - filename = xml_object.get('filename') - if filename is None: - definition_xml = copy.deepcopy(xml_object) - cls.clean_metadata_from_xml(definition_xml) - return {'data': stringify_children(definition_xml)}, [] - else: - # html is special. cls.filename_extension is 'xml', but - # if 'filename' is in the definition, that means to load - # from .html - # 'filename' in html pointers is a relative path - # (not same as 'html/blah.html' when the pointer is in a directory itself) - pointer_path = "{category}/{url_path}".format( - category='html', - url_path=name_to_pathname(location.block_id) - ) - base = path(pointer_path).dirname() - # log.debug("base = {0}, base.dirname={1}, filename={2}".format(base, base.dirname(), filename)) - filepath = f"{base}/{filename}.html" - # log.debug("looking for html file for {0} at {1}".format(location, filepath)) - - # VS[compat] - # TODO (cpennington): If the file doesn't exist at the right path, - # give the class a chance to fix it up. The file will be written out - # again in the correct format. This should go away once the CMS is - # online and has imported all current (fall 2012) courses from xml - if not system.resources_fs.exists(filepath): - - candidates = cls.backcompat_paths(filepath) - # log.debug("candidates = {0}".format(candidates)) - for candidate in candidates: - if system.resources_fs.exists(candidate): - filepath = candidate - break - - try: - with system.resources_fs.open(filepath, encoding='utf-8') as infile: - html = infile.read() - # Log a warning if we can't parse the file, but don't error - if not check_html(html) and len(html) > 0: - msg = f"Couldn't parse html in {filepath}, content = {html}" - log.warning(msg) - system.error_tracker("Warning: " + msg) - - definition = {'data': html} - - # TODO (ichuang): remove this after migration - # for Fall 2012 LMS migration: keep filename (and unmangled filename) - definition['filename'] = [filepath, filename] - - return definition, [] - - except ResourceNotFound as err: - msg = 'Unable to load file contents at path {}: {} '.format( # noqa: UP032 - filepath, err) - # add more info and re-raise - raise Exception(msg).with_traceback(sys.exc_info()[2]) # noqa: B904 - - @classmethod - def parse_xml_new_runtime(cls, node, runtime, keys): - """ - Parse XML in the new openedx_content-based runtime. Since it doesn't yet - support loading separate .html files, the HTML data is assumed to be in - a CDATA child or otherwise just inline in the OLX. - """ - block = runtime.construct_xblock_from_class(cls, keys) - block.data = stringify_children(node) - # Attributes become fields. - for name, value in node.items(): - cls._set_field_if_present(block, name, value, {}) - return block - - # TODO (vshnayder): make export put things in the right places. - - def definition_to_xml(self, resource_fs): - ''' Write to filename.xml, and the html - string to filename.html. - ''' - - # Write html to file, return an empty tag - pathname = name_to_pathname(self.url_name) - filepath = '{category}/{pathname}.html'.format( # noqa: UP032 - category=self.category, - pathname=pathname - ) - - resource_fs.makedirs(os.path.dirname(filepath), recreate=True) - with resource_fs.open(filepath, 'wb') as filestream: - html_data = self.data.encode('utf-8') - filestream.write(html_data) - - # write out the relative name - relname = path(pathname).basename() - - elt = etree.Element('html') - elt.set("filename", relname) - return elt - - @property - def non_editable_metadata_fields(self): - """ - `use_latex_compiler` should not be editable in the Studio settings editor. - """ - non_editable_fields = super().non_editable_metadata_fields - non_editable_fields.append(HtmlBlockMixin.use_latex_compiler) - return non_editable_fields - - def index_dictionary(self): - xblock_body = super().index_dictionary() - # Removing script and style - html_content = re.sub( - re.compile( - r""" - | - - """, - re.DOTALL | - re.VERBOSE), - "", - self.data - ) - html_content = escape_html_characters(html_content) - html_body = { - "html_content": html_content, - "display_name": self.display_name, - } - if "content" in xblock_body: - xblock_body["content"].update(html_body) - else: - xblock_body["content"] = html_body - xblock_body["content_type"] = "Text" - return xblock_body - - -@edxnotes -class _BuiltInHtmlBlock(_BuiltinHtmlBlockMixin): # pylint: disable=abstract-method - """ - This is the actual HTML XBlock. - Nothing extra is required; this is just a wrapper to include edxnotes support. - - .. deprecated:: 2026-03 - This built-in HTML block is deprecated. Please use the extracted ``HtmlBlock`` - from ``xblocks_contrib.html`` instead. - """ - is_extracted = False - - -HtmlBlockMixin = None - - -def reset_Mixin(): - """Reset Mixin as per django settings flag""" - global HtmlBlockMixin - HtmlBlockMixin = ( - _ExtractedHtmlBlockMixin if settings.USE_EXTRACTED_HTML_BLOCK - else _BuiltinHtmlBlockMixin - ) - return HtmlBlockMixin - -reset_Mixin() - - class AboutFields: # pylint: disable=missing-class-docstring display_name = String( help=_("The display name for this component."), @@ -530,30 +146,3 @@ def safe_parse_date(date): return datetime.strptime(date, '%B %d, %Y') except ValueError: # occurs for ill-formatted date values return datetime.today() - - -HtmlBlock = None - - -def reset_class(): - """Reset class as per django settings flag""" - global HtmlBlock - HtmlBlock = ( - _ExtractedHtmlBlock if settings.USE_EXTRACTED_HTML_BLOCK - else _BuiltInHtmlBlock - ) - return HtmlBlock - -reset_class() -HtmlBlock.__name__ = "HtmlBlock" - -if not settings.USE_EXTRACTED_HTML_BLOCK: - warnings.warn( - "The built-in `xmodule.html_block` HtmlBlock implementation is deprecated. " - "To fix this warning, enable `USE_EXTRACTED_HTML_BLOCK` (set it to True) to use " - "`xblocks_contrib.html.HtmlBlock` instead. " - "Support for the built-in implementation, and the `USE_EXTRACTED_HTML_BLOCK` setting, " - "will be removed in Willow.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/xmodule/static/css-builtin-blocks/HtmlBlockDisplay.css b/xmodule/static/css-builtin-blocks/HtmlBlockDisplay.css deleted file mode 100644 index ff8b7c7fae35..000000000000 --- a/xmodule/static/css-builtin-blocks/HtmlBlockDisplay.css +++ /dev/null @@ -1,507 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700"); - -.xmodule_display.xmodule_AboutBlock *, -.xmodule_display.xmodule_CourseInfoBlock *, -.xmodule_display.xmodule_HtmlBlock *, -.xmodule_display.xmodule_StaticTabBlock * { - line-height: 1.4em; -} - -.xmodule_display.xmodule_AboutBlock h1, -.xmodule_display.xmodule_CourseInfoBlock h1, -.xmodule_display.xmodule_HtmlBlock h1, -.xmodule_display.xmodule_StaticTabBlock h1 { - color: var(--body-color, #313131); - font: normal 2em/1.4em var(--font-family-sans-serif); - letter-spacing: 1px; - margin: 0 0 1.416em; -} - -.xmodule_display.xmodule_AboutBlock h2, -.xmodule_display.xmodule_CourseInfoBlock h2, -.xmodule_display.xmodule_HtmlBlock h2, -.xmodule_display.xmodule_StaticTabBlock h2 { - color: #646464; - font: normal 1.2em/1.2em var(--font-family-sans-serif); - letter-spacing: 1px; - margin-bottom: calc((var(--baseline, 20px) * 0.75)); - -webkit-font-smoothing: antialiased; -} - -.xmodule_display.xmodule_AboutBlock h3, -.xmodule_display.xmodule_AboutBlock h4, -.xmodule_display.xmodule_AboutBlock h5, -.xmodule_display.xmodule_AboutBlock h6, -.xmodule_display.xmodule_CourseInfoBlock h3, -.xmodule_display.xmodule_CourseInfoBlock h4, -.xmodule_display.xmodule_CourseInfoBlock h5, -.xmodule_display.xmodule_CourseInfoBlock h6, -.xmodule_display.xmodule_HtmlBlock h3, -.xmodule_display.xmodule_HtmlBlock h4, -.xmodule_display.xmodule_HtmlBlock h5, -.xmodule_display.xmodule_HtmlBlock h6, -.xmodule_display.xmodule_StaticTabBlock h3, -.xmodule_display.xmodule_StaticTabBlock h4, -.xmodule_display.xmodule_StaticTabBlock h5, -.xmodule_display.xmodule_StaticTabBlock h6 { - margin: 0 0 calc((var(--baseline, 20px) / 2)); - font-weight: 600; -} - -.xmodule_display.xmodule_AboutBlock h3, -.xmodule_display.xmodule_CourseInfoBlock h3, -.xmodule_display.xmodule_HtmlBlock h3, -.xmodule_display.xmodule_StaticTabBlock h3 { - font-size: 1.2em; -} - -.xmodule_display.xmodule_AboutBlock h4, -.xmodule_display.xmodule_CourseInfoBlock h4, -.xmodule_display.xmodule_HtmlBlock h4, -.xmodule_display.xmodule_StaticTabBlock h4 { - font-size: 1em; -} - -.xmodule_display.xmodule_AboutBlock h5, -.xmodule_display.xmodule_CourseInfoBlock h5, -.xmodule_display.xmodule_HtmlBlock h5, -.xmodule_display.xmodule_StaticTabBlock h5 { - font-size: 0.83em; -} - -.xmodule_display.xmodule_AboutBlock h6, -.xmodule_display.xmodule_CourseInfoBlock h6, -.xmodule_display.xmodule_HtmlBlock h6, -.xmodule_display.xmodule_StaticTabBlock h6 { - font-size: 0.75em; -} - -.xmodule_display.xmodule_AboutBlock p, -.xmodule_display.xmodule_CourseInfoBlock p, -.xmodule_display.xmodule_HtmlBlock p, -.xmodule_display.xmodule_StaticTabBlock p { - margin-bottom: 1.416em; - font-size: 1em; - line-height: 1.6em !important; - color: var(--body-color, #313131); -} - -.xmodule_display.xmodule_AboutBlock em, -.xmodule_display.xmodule_AboutBlock i, -.xmodule_display.xmodule_CourseInfoBlock em, -.xmodule_display.xmodule_CourseInfoBlock i, -.xmodule_display.xmodule_HtmlBlock em, -.xmodule_display.xmodule_HtmlBlock i, -.xmodule_display.xmodule_StaticTabBlock em, -.xmodule_display.xmodule_StaticTabBlock i { - font-style: italic; -} - -.xmodule_display.xmodule_AboutBlock em span, -.xmodule_display.xmodule_AboutBlock i span, -.xmodule_display.xmodule_CourseInfoBlock em span, -.xmodule_display.xmodule_CourseInfoBlock i span, -.xmodule_display.xmodule_HtmlBlock em span, -.xmodule_display.xmodule_HtmlBlock i span, -.xmodule_display.xmodule_StaticTabBlock em span, -.xmodule_display.xmodule_StaticTabBlock i span { - font-style: italic; -} - -.xmodule_display.xmodule_AboutBlock strong, -.xmodule_display.xmodule_AboutBlock b, -.xmodule_display.xmodule_CourseInfoBlock strong, -.xmodule_display.xmodule_CourseInfoBlock b, -.xmodule_display.xmodule_HtmlBlock strong, -.xmodule_display.xmodule_HtmlBlock b, -.xmodule_display.xmodule_StaticTabBlock strong, -.xmodule_display.xmodule_StaticTabBlock b { - font-weight: bold; -} - -.xmodule_display.xmodule_AboutBlock strong span, -.xmodule_display.xmodule_AboutBlock b span, -.xmodule_display.xmodule_CourseInfoBlock strong span, -.xmodule_display.xmodule_CourseInfoBlock b span, -.xmodule_display.xmodule_HtmlBlock strong span, -.xmodule_display.xmodule_HtmlBlock b span, -.xmodule_display.xmodule_StaticTabBlock strong span, -.xmodule_display.xmodule_StaticTabBlock b span { - font-weight: bold; -} - -.xmodule_display.xmodule_AboutBlock p+p, -.xmodule_display.xmodule_AboutBlock ul+p, -.xmodule_display.xmodule_AboutBlock ol+p, -.xmodule_display.xmodule_CourseInfoBlock p+p, -.xmodule_display.xmodule_CourseInfoBlock ul+p, -.xmodule_display.xmodule_CourseInfoBlock ol+p, -.xmodule_display.xmodule_HtmlBlock p+p, -.xmodule_display.xmodule_HtmlBlock ul+p, -.xmodule_display.xmodule_HtmlBlock ol+p, -.xmodule_display.xmodule_StaticTabBlock p+p, -.xmodule_display.xmodule_StaticTabBlock ul+p, -.xmodule_display.xmodule_StaticTabBlock ol+p { - margin-top: var(--baseline, 20px); -} - -.xmodule_display.xmodule_AboutBlock blockquote, -.xmodule_display.xmodule_CourseInfoBlock blockquote, -.xmodule_display.xmodule_HtmlBlock blockquote, -.xmodule_display.xmodule_StaticTabBlock blockquote { - margin: 1em calc((var(--baseline, 20px) * 2)); -} - -.xmodule_display.xmodule_AboutBlock ol, -.xmodule_display.xmodule_AboutBlock ul, -.xmodule_display.xmodule_CourseInfoBlock ol, -.xmodule_display.xmodule_CourseInfoBlock ul, -.xmodule_display.xmodule_HtmlBlock ol, -.xmodule_display.xmodule_HtmlBlock ul, -.xmodule_display.xmodule_StaticTabBlock ol, -.xmodule_display.xmodule_StaticTabBlock ul { - padding: 0 0 0 1em; - margin: 1em 0; - color: var(--body-color, #313131); -} - -.xmodule_display.xmodule_AboutBlock ol li, -.xmodule_display.xmodule_AboutBlock ul li, -.xmodule_display.xmodule_CourseInfoBlock ol li, -.xmodule_display.xmodule_CourseInfoBlock ul li, -.xmodule_display.xmodule_HtmlBlock ol li, -.xmodule_display.xmodule_HtmlBlock ul li, -.xmodule_display.xmodule_StaticTabBlock ol li, -.xmodule_display.xmodule_StaticTabBlock ul li { - margin-bottom: 0.708em; -} - -.xmodule_display.xmodule_AboutBlock ol, -.xmodule_display.xmodule_CourseInfoBlock ol, -.xmodule_display.xmodule_HtmlBlock ol, -.xmodule_display.xmodule_StaticTabBlock ol { - list-style: decimal outside none; -} - -.xmodule_display.xmodule_AboutBlock ul, -.xmodule_display.xmodule_CourseInfoBlock ul, -.xmodule_display.xmodule_HtmlBlock ul, -.xmodule_display.xmodule_StaticTabBlock ul { - list-style: disc outside none; -} - -.xmodule_display.xmodule_AboutBlock a:link, -.xmodule_display.xmodule_AboutBlock a:visited, -.xmodule_display.xmodule_AboutBlock a:hover, -.xmodule_display.xmodule_AboutBlock a:active, -.xmodule_display.xmodule_AboutBlock a:focus, -.xmodule_display.xmodule_CourseInfoBlock a:link, -.xmodule_display.xmodule_CourseInfoBlock a:visited, -.xmodule_display.xmodule_CourseInfoBlock a:hover, -.xmodule_display.xmodule_CourseInfoBlock a:active, -.xmodule_display.xmodule_CourseInfoBlock a:focus, -.xmodule_display.xmodule_HtmlBlock a:link, -.xmodule_display.xmodule_HtmlBlock a:visited, -.xmodule_display.xmodule_HtmlBlock a:hover, -.xmodule_display.xmodule_HtmlBlock a:active, -.xmodule_display.xmodule_HtmlBlock a:focus, -.xmodule_display.xmodule_StaticTabBlock a:link, -.xmodule_display.xmodule_StaticTabBlock a:visited, -.xmodule_display.xmodule_StaticTabBlock a:hover, -.xmodule_display.xmodule_StaticTabBlock a:active, -.xmodule_display.xmodule_StaticTabBlock a:focus { - color: var(--blue, #0075b4); -} - -.xmodule_display.xmodule_AboutBlock img, -.xmodule_display.xmodule_CourseInfoBlock img, -.xmodule_display.xmodule_HtmlBlock img, -.xmodule_display.xmodule_StaticTabBlock img { - max-width: 100%; - height: auto; -} - -.xmodule_display.xmodule_AboutBlock pre, -.xmodule_display.xmodule_CourseInfoBlock pre, -.xmodule_display.xmodule_HtmlBlock pre, -.xmodule_display.xmodule_StaticTabBlock pre { - margin: 1em 0; - color: var(--body-color, #313131); - font-family: monospace, serif; - font-size: 1em; - white-space: pre-wrap; - word-wrap: break-word; -} - -.xmodule_display.xmodule_AboutBlock code, -.xmodule_display.xmodule_CourseInfoBlock code, -.xmodule_display.xmodule_HtmlBlock code, -.xmodule_display.xmodule_StaticTabBlock code { - color: var(--body-color, #313131); - font-family: monospace, serif; - background: none; - padding: 0; -} - -.xmodule_display.xmodule_AboutBlock table, -.xmodule_display.xmodule_CourseInfoBlock table, -.xmodule_display.xmodule_HtmlBlock table, -.xmodule_display.xmodule_StaticTabBlock table { - width: 100%; - margin: var(--baseline, 20px) 0; - border-collapse: collapse; - font-size: 16px; -} - -.xmodule_display.xmodule_AboutBlock table td, -.xmodule_display.xmodule_AboutBlock table th, -.xmodule_display.xmodule_CourseInfoBlock table td, -.xmodule_display.xmodule_CourseInfoBlock table th, -.xmodule_display.xmodule_HtmlBlock table td, -.xmodule_display.xmodule_HtmlBlock table th, -.xmodule_display.xmodule_StaticTabBlock table td, -.xmodule_display.xmodule_StaticTabBlock table th { - margin: var(--baseline, 20px) 0; - padding: calc((var(--baseline, 20px) / 2)); - border: 1px solid var(--gray-l3, #c8c8c8); - font-size: 14px; -} - -.xmodule_display.xmodule_AboutBlock table td.cont-justified-left, -.xmodule_display.xmodule_AboutBlock table th.cont-justified-left, -.xmodule_display.xmodule_CourseInfoBlock table td.cont-justified-left, -.xmodule_display.xmodule_CourseInfoBlock table th.cont-justified-left, -.xmodule_display.xmodule_HtmlBlock table td.cont-justified-left, -.xmodule_display.xmodule_HtmlBlock table th.cont-justified-left, -.xmodule_display.xmodule_StaticTabBlock table td.cont-justified-left, -.xmodule_display.xmodule_StaticTabBlock table th.cont-justified-left { - text-align: left !important; -} - -.xmodule_display.xmodule_AboutBlock table td.cont-justified-right, -.xmodule_display.xmodule_AboutBlock table th.cont-justified-right, -.xmodule_display.xmodule_CourseInfoBlock table td.cont-justified-right, -.xmodule_display.xmodule_CourseInfoBlock table th.cont-justified-right, -.xmodule_display.xmodule_HtmlBlock table td.cont-justified-right, -.xmodule_display.xmodule_HtmlBlock table th.cont-justified-right, -.xmodule_display.xmodule_StaticTabBlock table td.cont-justified-right, -.xmodule_display.xmodule_StaticTabBlock table th.cont-justified-right { - text-align: right !important; -} - -.xmodule_display.xmodule_AboutBlock table td.cont-justified-center, -.xmodule_display.xmodule_AboutBlock table th.cont-justified-center, -.xmodule_display.xmodule_CourseInfoBlock table td.cont-justified-center, -.xmodule_display.xmodule_CourseInfoBlock table th.cont-justified-center, -.xmodule_display.xmodule_HtmlBlock table td.cont-justified-center, -.xmodule_display.xmodule_HtmlBlock table th.cont-justified-center, -.xmodule_display.xmodule_StaticTabBlock table td.cont-justified-center, -.xmodule_display.xmodule_StaticTabBlock table th.cont-justified-center { - text-align: center !important; -} - -.xmodule_display.xmodule_AboutBlock th, -.xmodule_display.xmodule_CourseInfoBlock th, -.xmodule_display.xmodule_HtmlBlock th, -.xmodule_display.xmodule_StaticTabBlock th { - background: #eee; - font-weight: bold; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .modal-ui-icon, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .modal-ui-icon, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .modal-ui-icon, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .modal-ui-icon { - position: absolute; - display: block; - padding: calc((var(--baseline, 20px) / 4)) 7px; - border-radius: 5px; - opacity: 0.9; - background: var(--white, #fff); - color: var(--black, #000); - border: 2px solid var(--black, #000); -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .modal-ui-icon .label, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .modal-ui-icon .label, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .modal-ui-icon .label, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .modal-ui-icon .label { - font-weight: bold; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .modal-ui-icon i, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .modal-ui-icon i, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .modal-ui-icon i, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .modal-ui-icon i { - font-style: normal; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-link, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-link, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-link, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-link { - position: relative; - display: block; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-link .action-fullscreen, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-link .action-fullscreen, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-link .action-fullscreen, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-link .action-fullscreen { - display: none; - top: 10px; - left: 10px; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-link:hover .action-fullscreen, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-link:hover .action-fullscreen, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-link:hover .action-fullscreen, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-link:hover .action-fullscreen { - display: block; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal { - position: fixed; - top: 0; - left: 0; - display: none; - height: 100%; - width: 100%; - background-color: rgba(0, 0, 0, 0.7); -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content { - position: relative; - top: 2.5%; - display: block; - height: 95%; - width: 95%; - margin: auto; - overflow: hidden; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-wrapper, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-wrapper, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-wrapper, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-wrapper { - position: relative; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-wrapper img, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-wrapper img, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-wrapper img, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-wrapper img { - position: relative; - display: block; - max-width: 100%; - max-height: 100%; - margin: auto; - cursor: default; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .action-close, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .action-close, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .action-close, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .action-close { - top: 10px; - right: 10px; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-controls, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-controls, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-controls, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-controls { - position: absolute; - right: 10px; - bottom: 10px; - margin: 0; - padding: 0; - list-style: none; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control { - position: relative; - display: inline-block; - margin: 0; - padding: 0; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon { - position: relative; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-in, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-in, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-in, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-in { - margin-right: calc((var(--baseline, 20px) / 4)); -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-out, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-out, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-out, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.action-zoom-out { - margin-left: calc((var(--baseline, 20px) / 4)); -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.is-disabled, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.is-disabled, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.is-disabled, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal .image-content .image-controls .image-control .modal-ui-icon.is-disabled { - opacity: 0.5; - cursor: default; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen { - display: block; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper { - top: 0 !important; - left: 0 !important; - width: 100% !important; - height: 100% !important; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper img, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper img, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper img, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal.image-is-fit-to-screen .image-content .image-wrapper img { - top: 0 !important; - left: 0 !important; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal.image-is-zoomed, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal.image-is-zoomed, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal.image-is-zoomed, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal.image-is-zoomed { - display: block; -} - -.xmodule_display.xmodule_AboutBlock .wrapper-modal-image .image-modal.image-is-zoomed .image-content .image-wrapper img, -.xmodule_display.xmodule_CourseInfoBlock .wrapper-modal-image .image-modal.image-is-zoomed .image-content .image-wrapper img, -.xmodule_display.xmodule_HtmlBlock .wrapper-modal-image .image-modal.image-is-zoomed .image-content .image-wrapper img, -.xmodule_display.xmodule_StaticTabBlock .wrapper-modal-image .image-modal.image-is-zoomed .image-content .image-wrapper img { - max-width: none; - max-height: none; - margin: 0; - cursor: move; -} diff --git a/xmodule/tests/test_html_block.py b/xmodule/tests/test_html_block.py index e04993d4d2ad..e83c0a6ead62 100644 --- a/xmodule/tests/test_html_block.py +++ b/xmodule/tests/test_html_block.py @@ -3,246 +3,11 @@ import unittest from unittest.mock import Mock -import ddt -from django.contrib.auth.models import AnonymousUser -from django.test import TestCase -from django.test.utils import override_settings -from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from xblock.field_data import DictFieldData -from xblock.fields import ScopeIds -from xmodule import html_block from xmodule.html_block import CourseInfoBlock -from ..x_module import PUBLIC_VIEW, STUDENT_VIEW -from . import get_test_descriptor_system, get_test_system - - -@ddt.ddt -class _HtmlBlockCourseApiTestCaseBase(TestCase): - """ - Test the HTML XModule's student_view_data method. - """ - - __test__ = False - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.html_class = html_block.reset_class() - - @ddt.data( - {}, - dict(FEATURES={}), - dict(FEATURES=dict(ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA=False)) - ) - def test_disabled(self, settings): - """ - Ensure that student_view_data does not return html if the ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA feature flag - is not set. - """ - field_data = DictFieldData({'data': '

Some HTML

'}) - module_system = get_test_system() - block = self.html_class(module_system, field_data, Mock()) - - with override_settings(**settings): - assert block.student_view_data() ==\ - dict(enabled=False, message='To enable, set FEATURES["ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA"]') - - @ddt.data( - '

Some content

', # Valid HTML - '', - None, - '

Some contentalert()', # Does not escape tags - '', # Images allowed - 'short string ' * 100, # May contain long strings - ) - @override_settings(FEATURES=dict(ENABLE_HTML_XBLOCK_STUDENT_VIEW_DATA=True)) - def test_common_values(self, html): - """ - Ensure that student_view_data will return HTML data when enabled, - can handle likely input, - and doesn't modify the HTML in any way. - - This means that it does NOT protect against XSS, escape HTML tags, etc. - - Note that the %%USER_ID%% substitution is tested below. - """ - field_data = DictFieldData({'data': html}) - module_system = get_test_system() - block = self.html_class(module_system, field_data, Mock()) - assert block.student_view_data() == dict(enabled=True, html=html) - - @ddt.data( - STUDENT_VIEW, - PUBLIC_VIEW, - ) - def test_student_preview_view(self, view): - """ - Ensure that student_view and public_view renders correctly. - """ - html = '

This is a test

' - field_data = DictFieldData({'data': html}) - module_system = get_test_system() - block = self.html_class(module_system, field_data, Mock()) - rendered = module_system.render(block, view, {}).content - assert html in rendered - - -class _HtmlBlockSubstitutionTestCaseBase(TestCase): # pylint: disable=missing-class-docstring - - __test__ = False - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.html_class = html_block.reset_class() - - def test_substitution_user_id(self): - sample_xml = '''%%USER_ID%%''' - field_data = DictFieldData({'data': sample_xml}) - module_system = get_test_system() - block = self.html_class(module_system, field_data, Mock()) - assert block.get_html() == str(module_system.anonymous_student_id) - - def test_substitution_course_id(self): - sample_xml = '''%%COURSE_ID%%''' - field_data = DictFieldData({'data': sample_xml}) - module_system = get_test_system() - block = self.html_class(module_system, field_data, Mock()) - course_key = CourseLocator( - org='some_org', - course='some_course', - run='some_run' - ) - usage_key = BlockUsageLocator( - course_key=course_key, - block_type='problem', - block_id='block_id' - ) - block.scope_ids.usage_id = usage_key - assert block.get_html() == str(course_key) - - def test_substitution_without_magic_string(self): - sample_xml = ''' - -

Hi USER_ID!11!

- - ''' - field_data = DictFieldData({'data': sample_xml}) - module_system = get_test_system() - block = self.html_class(module_system, field_data, Mock()) - assert block.get_html() == sample_xml - - def test_substitution_without_anonymous_student_id(self): - sample_xml = '''%%USER_ID%%''' - field_data = DictFieldData({'data': sample_xml}) - module_system = get_test_system(user=AnonymousUser()) - block = self.html_class(module_system, field_data, Mock()) - block.runtime.service(block, 'user')._deprecated_anonymous_user_id = '' # pylint: disable=protected-access - assert block.get_html() == sample_xml - - -class _HtmlBlockIndexingTestCaseBase(TestCase): - """ - Make sure that HtmlBlock can format data for indexing as expected. - """ - - __test__ = False - - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.html_class = html_block.reset_class() - - def instantiate_block(self, **field_data): - """ - Instantiate HtmlBlock with field data. - """ - system = get_test_descriptor_system() - course_key = CourseLocator('org', 'course', 'run') - usage_key = course_key.make_usage_key('html', 'SampleHtml') - return system.construct_xblock_from_class( - self.html_class, - scope_ids=ScopeIds(None, None, usage_key, usage_key), - field_data=DictFieldData(field_data), - ) - - def test_index_dictionary_simple_html_block(self): - sample_xml = ''' - -

Hello World!

- - ''' - block = self.instantiate_block(data=sample_xml) - assert block.index_dictionary() ==\ - {'content': {'html_content': ' Hello World! ', 'display_name': 'Text'}, 'content_type': 'Text'} - - def test_index_dictionary_cdata_html_block(self): - sample_xml_cdata = ''' - -

This has CDATA in it.

- - - ''' - block = self.instantiate_block(data=sample_xml_cdata) - assert block.index_dictionary() ==\ - {'content': {'html_content': ' This has CDATA in it. ', 'display_name': 'Text'}, 'content_type': 'Text'} - - def test_index_dictionary_multiple_spaces_html_block(self): - sample_xml_tab_spaces = ''' - -

Text has spaces :)

- - ''' - block = self.instantiate_block(data=sample_xml_tab_spaces) - assert block.index_dictionary() ==\ - {'content': {'html_content': ' Text has spaces :) ', 'display_name': 'Text'}, 'content_type': 'Text'} - - def test_index_dictionary_html_block_with_comment(self): - sample_xml_comment = ''' - -

This has HTML comment in it.

- - - ''' - block = self.instantiate_block(data=sample_xml_comment) - assert block.index_dictionary() == {'content': {'html_content': ' This has HTML comment in it. ', 'display_name': 'Text'}, 'content_type': 'Text'} # pylint: disable=line-too-long - - def test_index_dictionary_html_block_with_both_comments_and_cdata(self): - sample_xml_mix_comment_cdata = ''' - - -

This has HTML comment in it.

- - -

HTML end.

- - ''' - block = self.instantiate_block(data=sample_xml_mix_comment_cdata) - assert block.index_dictionary() ==\ - {'content': {'html_content': ' This has HTML comment in it. HTML end. ', - 'display_name': 'Text'}, 'content_type': 'Text'} - - def test_index_dictionary_html_block_with_script_and_style_tags(self): - sample_xml_style_script_tags = ''' - - - -

This has HTML comment in it.

- - -

HTML end.

- - - ''' - block = self.instantiate_block(data=sample_xml_style_script_tags) - assert block.index_dictionary() ==\ - {'content': {'html_content': ' This has HTML comment in it. HTML end. ', - 'display_name': 'Text'}, 'content_type': 'Text'} +from . import get_test_system class CourseInfoBlockTestCase(unittest.TestCase): @@ -311,7 +76,6 @@ def test_updates_order(self): Mock() ) - # This is the expected context that should be used by the render function expected_context = { 'visible_updates': [ { @@ -337,38 +101,7 @@ def test_updates_order(self): } template_name = f"{info_block.TEMPLATE_DIR}/course_updates.html" info_block.get_html() - # Assertion to validate that render function is called with the expected context info_block.runtime.service(info_block, 'mako').render_lms_template.assert_called_once_with( template_name, expected_context ) - - -@override_settings(USE_EXTRACTED_HTML_BLOCK=True) -class ExtractedHtmlBlockCourseApiTestCase(_HtmlBlockCourseApiTestCaseBase): - __test__ = True - - -@override_settings(USE_EXTRACTED_HTML_BLOCK=False) -class BuiltInHtmlBlockCourseApiTestCase(_HtmlBlockCourseApiTestCaseBase): - __test__ = True - - -@override_settings(USE_EXTRACTED_HTML_BLOCK=True) -class ExtractedHtmlBlockSubstitutionTestCase(_HtmlBlockSubstitutionTestCaseBase): - __test__ = True - - -@override_settings(USE_EXTRACTED_HTML_BLOCK=False) -class BuiltInHtmlBlockSubstitutionTestCase(_HtmlBlockSubstitutionTestCaseBase): - __test__ = True - - -@override_settings(USE_EXTRACTED_HTML_BLOCK=True) -class ExtractedHtmlBlockIndexingTestCase(_HtmlBlockIndexingTestCaseBase): - __test__ = True - - -@override_settings(USE_EXTRACTED_HTML_BLOCK=False) -class BuiltInHtmlBlockIndexingTestCase(_HtmlBlockIndexingTestCaseBase): - __test__ = True From 2c5631559343281eaac212ee70dacab84761a414 Mon Sep 17 00:00:00 2001 From: Irfan Ahmad Date: Fri, 12 Jun 2026 16:30:42 +0500 Subject: [PATCH 2/4] fix: restore html entrypoint pointing to xblocks_contrib Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 108b2a9c8eee..09dfc3df3f90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ discussion = "xmodule.discussion_block:DiscussionXBlock" error = "xmodule.error_block:ErrorBlock" hidden = "xmodule.hidden_block:HiddenBlock" itembank = "xmodule.item_bank_block:ItemBankBlock" +html = "xblocks_contrib:HtmlBlock" image = "xmodule.template_block:TranslateCustomTagBlock" library = "xmodule.library_root_xblock:LibraryRoot" library_content = "xmodule.library_content_block:LegacyLibraryContentBlock" From 430730df2840c480be5171751bb0639350dba971 Mon Sep 17 00:00:00 2001 From: Irfan Ahmad Date: Fri, 12 Jun 2026 16:49:51 +0500 Subject: [PATCH 3/4] fix: sort imports (ruff isort) after xblocks_contrib migration Co-Authored-By: Claude Sonnet 4.6 --- lms/djangoapps/courseware/tests/test_block_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/tests/test_block_render.py b/lms/djangoapps/courseware/tests/test_block_render.py index ae1c9ebb4ec6..f1e4e1817033 100644 --- a/lms/djangoapps/courseware/tests/test_block_render.py +++ b/lms/djangoapps/courseware/tests/test_block_render.py @@ -51,6 +51,7 @@ Mixologist, # pylint: disable=wrong-import-order ) from xblock.test.tools import TestRuntime # pylint: disable=wrong-import-order +from xblocks_contrib.html import HtmlBlock from xblocks_contrib.problem.capa.tests.response_xml_factory import ( OptionResponseXMLFactory, # pylint: disable=reimported ) @@ -96,7 +97,6 @@ from openedx.core.lib.url_utils import quote_slashes from xmodule.capa_block import ProblemBlock from xmodule.contentstore.django import contentstore -from xblocks_contrib.html import HtmlBlock from xmodule.html_block import AboutBlock, CourseInfoBlock, StaticTabBlock from xmodule.lti_block import LTIBlock from xmodule.modulestore import ModuleStoreEnum From 998f59c66eee22ca86c9bc55eab67736d4a02488 Mon Sep 17 00:00:00 2001 From: Irfan Ahmad Date: Fri, 12 Jun 2026 18:03:12 +0500 Subject: [PATCH 4/4] fix: update HtmlBlock mock.patch paths to xblocks_contrib.html Co-Authored-By: Claude Sonnet 4.6 --- .../contentstore/views/tests/test_course_index.py | 6 +++--- xmodule/tests/test_library_content.py | 4 ++-- xmodule/tests/test_library_root.py | 4 ++-- xmodule/tests/test_split_test_block.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cms/djangoapps/contentstore/views/tests/test_course_index.py b/cms/djangoapps/contentstore/views/tests/test_course_index.py index 8c33ad3f891f..06888004d7b5 100644 --- a/cms/djangoapps/contentstore/views/tests/test_course_index.py +++ b/cms/djangoapps/contentstore/views/tests/test_course_index.py @@ -314,7 +314,7 @@ def test_empty_content_type(self): self.assertContains(response, self.SUCCESSFUL_RESPONSE) self.assertEqual(response.status_code, 200) # noqa: PT009 - @mock.patch('xmodule.html_block.HtmlBlock.index_dictionary') + @mock.patch('xblocks_contrib.html.HtmlBlock.index_dictionary') def test_reindex_course_search_index_error(self, mock_index_dictionary): """ Test json response with mocked error data for html @@ -377,7 +377,7 @@ def test_reindex_video_error_json_responses(self, mock_index_dictionary): with self.assertRaises(SearchIndexingError): # noqa: PT027 reindex_course_and_check_access(self.course.id, self.user) - @mock.patch('xmodule.html_block.HtmlBlock.index_dictionary') + @mock.patch('xblocks_contrib.html.HtmlBlock.index_dictionary') def test_reindex_html_error_json_responses(self, mock_index_dictionary): """ Test json response with mocked error data for html @@ -487,7 +487,7 @@ def test_indexing_video_error_responses(self, mock_index_dictionary): with self.assertRaises(SearchIndexingError): # noqa: PT027 CoursewareSearchIndexer.do_course_reindex(modulestore(), self.course.id) - @mock.patch('xmodule.html_block.HtmlBlock.index_dictionary') + @mock.patch('xblocks_contrib.html.HtmlBlock.index_dictionary') def test_indexing_html_error_responses(self, mock_index_dictionary): """ Test do_course_reindex response with mocked error data for html diff --git a/xmodule/tests/test_library_content.py b/xmodule/tests/test_library_content.py index d5a7fcbc1cc5..1b433c695043 100644 --- a/xmodule/tests/test_library_content.py +++ b/xmodule/tests/test_library_content.py @@ -523,7 +523,7 @@ def setUp(self): @patch( 'xmodule.modulestore.split_mongo.runtime.SplitModuleStoreRuntime.render', VanillaRuntime.render ) -@patch('xmodule.html_block.HtmlBlock.author_view', dummy_render, create=True) +@patch('xblocks_contrib.html.HtmlBlock.author_view', dummy_render, create=True) @patch('xmodule.x_module.ModuleStoreRuntime.applicable_aside_types', lambda self, block: []) class TestLibraryContentRender(LegacyLibraryContentTest): """ @@ -734,7 +734,7 @@ def test_removed_invalid(self): @patch( 'xmodule.modulestore.split_mongo.runtime.SplitModuleStoreRuntime.render', VanillaRuntime.render ) -@patch('xmodule.html_block.HtmlBlock.author_view', dummy_render, create=True) +@patch('xblocks_contrib.html.HtmlBlock.author_view', dummy_render, create=True) @patch('xmodule.x_module.ModuleStoreRuntime.applicable_aside_types', lambda self, block: []) class TestLegacyLibraryContentBlockMigration(LegacyLibraryContentTest): """ diff --git a/xmodule/tests/test_library_root.py b/xmodule/tests/test_library_root.py index caf3e52a533e..a33c18b6a8fd 100644 --- a/xmodule/tests/test_library_root.py +++ b/xmodule/tests/test_library_root.py @@ -18,8 +18,8 @@ @patch( 'xmodule.modulestore.split_mongo.runtime.SplitModuleStoreRuntime.render', VanillaRuntime.render ) -@patch('xmodule.html_block.HtmlBlock.author_view', dummy_render, create=True) -@patch('xmodule.html_block.HtmlBlock.has_author_view', True, create=True) +@patch('xblocks_contrib.html.HtmlBlock.author_view', dummy_render, create=True) +@patch('xblocks_contrib.html.HtmlBlock.has_author_view', True, create=True) @patch('xmodule.x_module.ModuleStoreRuntime.applicable_aside_types', lambda self, block: []) class TestLibraryRoot(MixedSplitTestCase): """ diff --git a/xmodule/tests/test_split_test_block.py b/xmodule/tests/test_split_test_block.py index 09854343ddd0..8820426dbc77 100644 --- a/xmodule/tests/test_split_test_block.py +++ b/xmodule/tests/test_split_test_block.py @@ -158,7 +158,7 @@ def test_child_persist_new_tag_value_when_tag_missing(self, _user_tag): # noqa: assert self.split_test_block.child_block.url_name == self.split_test_block.child_block.url_name # Patch the definition_to_xml for the html children. - @patch('xmodule.html_block.HtmlBlock.definition_to_xml') + @patch('xblocks_contrib.html.HtmlBlock.definition_to_xml') def test_export_import_round_trip(self, def_to_xml): # The HtmlBlock definition_to_xml tries to write to the filesystem # before returning an xml object. Patch this to just return the xml.