From bfe407eb9d5599d95ceacce554334c7af8a0a1ce Mon Sep 17 00:00:00 2001 From: farhan Date: Tue, 31 Mar 2026 16:20:28 +0500 Subject: [PATCH 1/2] test: fail fast false --- .github/workflows/unit-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 7b7b14a43e88..a5ad848a26c1 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -20,6 +20,7 @@ jobs: name: ${{ matrix.shard_name }}(py=${{ matrix.python-version }},dj=${{ matrix.django-version }},mongo=${{ matrix.mongo-version }}) runs-on: ${{ matrix.os-version }} strategy: + fail-fast: false matrix: python-version: - "3.12" From 95f6ef1ab84b235112588988d64073f1cae7c472 Mon Sep 17 00:00:00 2001 From: farhan Date: Tue, 31 Mar 2026 17:23:38 +0500 Subject: [PATCH 2/2] test: drop the addition of XModuleMixin for the xblocks-contrib blocks --- .../contentstore/views/component.py | 4 ++- cms/djangoapps/contentstore/views/preview.py | 3 +- .../xblock/runtime/openedx_content_runtime.py | 6 +++- openedx/core/djangoapps/xblock/utils.py | 33 +++++++++++++++++++ openedx/envs/common.py | 14 ++++++++ xmodule/modulestore/__init__.py | 4 ++- xmodule/modulestore/mongo/base.py | 7 ++-- xmodule/modulestore/split_mongo/split.py | 9 +++-- xmodule/modulestore/xml.py | 8 +++-- 9 files changed, 78 insertions(+), 10 deletions(-) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 8b16fdbad80d..8c087e03b490 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -37,6 +37,7 @@ ) from cms.djangoapps.contentstore.xblock_storage_handlers.view_handlers import load_services_for_studio from openedx.core.lib.xblock_utils import get_aside_from_xblock, is_xblock_aside +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration from openedx.core.djangoapps.content_tagging.api import get_object_tags from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order @@ -132,7 +133,8 @@ def _load_mixed_class(category): return None component_class = XBlock.load_class(category) - mixologist = Mixologist(settings.XBLOCK_MIXINS) + mixins = filter_mixins_for_standard_xblocks(component_class, settings.XBLOCK_MIXINS) + mixologist = Mixologist(mixins) return mixologist.mix(component_class) diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index 6600430a7a89..2f3a8c62e053 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -38,6 +38,7 @@ from common.djangoapps.student.models import anonymous_id_for_user from common.djangoapps.edxmako.shortcuts import render_to_string from common.djangoapps.edxmako.services import MakoService +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks from common.djangoapps.xblock_django.user_service import DjangoXBlockUserService from lms.djangoapps.lms_xblock.field_data import LmsFieldData from openedx.core.lib.license import wrap_with_license @@ -234,7 +235,7 @@ def _prepare_runtime_for_preview(request, block): } block.runtime.get_block_for_descriptor = partial(_load_preview_block, request) - block.runtime.mixins = settings.XBLOCK_MIXINS + block.runtime.mixins = filter_mixins_for_standard_xblocks(block.__class__, settings.XBLOCK_MIXINS) # Set up functions to modify the fragment produced by student_view block.runtime.wrappers = wrappers diff --git a/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py b/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py index e7fa2d0a7b82..a7ccf66a1462 100644 --- a/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py +++ b/openedx/core/djangoapps/xblock/runtime/openedx_content_runtime.py @@ -20,6 +20,7 @@ from xblock.exceptions import NoSuchUsage from xblock.fields import Field, Scope, ScopeIds from xblock.field_data import FieldData +from xblock.runtime import Mixologist from openedx.core.djangoapps.xblock.api import get_xblock_app_config from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_openedx_content @@ -28,6 +29,7 @@ from ..utils import get_auto_latest_version from ..learning_context.manager import get_learning_context_impl from .runtime import XBlockRuntime +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks log = logging.getLogger(__name__) @@ -203,7 +205,9 @@ def get_block(self, usage_key, for_parent=None, *, version: int | LatestVersion if xml_node.get("url_name", None): log.warning("XBlock at %s should not specify an old-style url_name attribute.", usage_key) - block_class = self.mixologist.mix(self.load_block_type(block_type)) + base_class = self.load_block_type(block_type) + mixins = filter_mixins_for_standard_xblocks(base_class, mixologist=self.mixologist) + block_class = Mixologist(mixins).mix(base_class) if hasattr(block_class, 'parse_xml_new_runtime'): # This is a (former) XModule with messy XML parsing code; let its parse_xml() method continue to work diff --git a/openedx/core/djangoapps/xblock/utils.py b/openedx/core/djangoapps/xblock/utils.py index b4ae054cf498..e8980c3eb93d 100644 --- a/openedx/core/djangoapps/xblock/utils.py +++ b/openedx/core/djangoapps/xblock/utils.py @@ -4,6 +4,7 @@ import hashlib import hmac +import inspect import math import time from uuid import uuid4 @@ -14,6 +15,7 @@ from openedx.core.djangoapps.xblock.apps import get_xblock_app_config from .data import AuthoredDataMode, LatestVersion +from xmodule.x_module import XModuleMixin def get_secure_token_for_xblock_handler(user_id, block_key_str, time_idx=0): @@ -186,3 +188,34 @@ def get_auto_latest_version(version: int | LatestVersion) -> int | LatestVersion else LatestVersion.PUBLISHED ) return version + + +def filter_mixins_for_standard_xblocks(xblock_class, mixins=None, mixologist=None): + """ + Filter legacy mixins for standard-compliant extracted XBlocks. + + If the fully-qualified class name of ``xblock_class`` is listed in + ``settings.STANDARD_COMPLIANT_XBLOCKS``, then we remove ``XModuleMixin`` from the + provided ``mixins``. + """ + if mixins is None: + mixins = () + if mixologist is not None: + mixins = getattr(mixologist, "_mixins", mixins) + if not xblock_class: + return mixins + + full_class_name = f"{xblock_class.__module__}.{xblock_class.__name__}" + + if full_class_name not in getattr(settings, "STANDARD_COMPLIANT_XBLOCKS", ()): + return mixins + + filtered_mixins = tuple(m for m in mixins if m is not XModuleMixin) + + if XModuleMixin not in filtered_mixins: + caller = None + frame = inspect.currentframe() + if frame is not None and frame.f_back is not None: + caller = f"{frame.f_back.f_code.co_filename}:{frame.f_back.f_lineno} ({frame.f_back.f_code.co_name})" + print(f"XModuleMixin removed/not-added in {full_class_name} mixins: {filtered_mixins} | caller={caller}") + return filtered_mixins diff --git a/openedx/envs/common.py b/openedx/envs/common.py index 62669644d681..132da68d5a2f 100644 --- a/openedx/envs/common.py +++ b/openedx/envs/common.py @@ -2030,6 +2030,20 @@ def add_optional_apps(optional_apps, installed_apps): EditInfoMixin, ) +# .. setting_name: STANDARD_COMPLIANT_XBLOCKS +# .. setting_default: () +# .. setting_description: Tuple of fully-qualified XBlock class names that conform to the standard XBlock +# interface. For these XBlocks, the platform will not inject legacy mixins like XModuleMixin when +# instantiating them via the various XBlock runtimes. +STANDARD_COMPLIANT_XBLOCKS = ( + "xblocks_contrib.video.video.VideoBlock", + "xblocks_contrib.word_cloud.word_cloud.WordCloudBlock", + "xblocks_contrib.discussion.discussion.DiscussionXBlock", + "xblocks_contrib.poll.poll.PollBlock", + "xblocks_contrib.html.html.HtmlBlockMixin", + "xblocks_contrib.annotatable.annotatable.AnnotatableBlock", +) + ######################## Built-in Blocks Extraction ######################## # The following Django settings flags have been introduced temporarily to facilitate diff --git a/xmodule/modulestore/__init__.py b/xmodule/modulestore/__init__.py index 3446a4c8c61b..c20a2ac8ef41 100644 --- a/xmodule/modulestore/__init__.py +++ b/xmodule/modulestore/__init__.py @@ -21,6 +21,7 @@ from xblock.core import XBlock from xblock.plugin import default_select from xblock.runtime import Mixologist +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks # The below import is not used within this module, but ir is still needed becuase # other modules are imorting EdxJSONEncoder from here @@ -1311,7 +1312,8 @@ def partition_fields_by_scope(self, category, fields): if fields is None: return result classes = XBlock.load_class(category, default=self.default_class) - cls = self.mixologist.mix(classes) + mixins = filter_mixins_for_standard_xblocks(classes, mixologist=self.mixologist) + cls = Mixologist(mixins).mix(classes) for field_name, value in fields.items(): field = getattr(cls, field_name) result[field.scope][field_name] = value diff --git a/xmodule/modulestore/mongo/base.py b/xmodule/modulestore/mongo/base.py index 4282dbd02ae8..a7186e99bc4d 100644 --- a/xmodule/modulestore/mongo/base.py +++ b/xmodule/modulestore/mongo/base.py @@ -29,7 +29,9 @@ from path import Path as path from xblock.exceptions import InvalidScopeError from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope, ScopeIds -from xblock.runtime import KvsFieldData +from xblock.runtime import KvsFieldData, Mixologist + +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks from xmodule.assetstore import AssetMetadata, CourseAssetsFromStorage from xmodule.course_block import CourseSummary @@ -229,7 +231,8 @@ def load_item(self, location, for_parent=None): # lint-amnesty, pylint: disable if isinstance(data, str): data = {'data': data} - mixed_class = self.mixologist.mix(class_) + mixins = filter_mixins_for_standard_xblocks(class_, mixologist=self.mixologist) + mixed_class = Mixologist(mixins).mix(class_) if data: # empty or None means no work data = self._convert_reference_fields_to_keys(mixed_class, location.course_key, data) metadata = self._convert_reference_fields_to_keys(mixed_class, location.course_key, metadata) diff --git a/xmodule/modulestore/split_mongo/split.py b/xmodule/modulestore/split_mongo/split.py index aeacac25333e..47d9dee09a40 100644 --- a/xmodule/modulestore/split_mongo/split.py +++ b/xmodule/modulestore/split_mongo/split.py @@ -75,6 +75,9 @@ from path import Path as path from xblock.core import XBlock from xblock.fields import Reference, ReferenceList, ReferenceValueDict, Scope +from xblock.runtime import Mixologist + +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks from xmodule.assetstore import AssetMetadata from xmodule.course_block import CourseSummary @@ -2925,7 +2928,8 @@ def robust_usage_key(block_key): except KeyError: return course_key.make_usage_key('unknown', block_key.id) - xblock_class = self.mixologist.mix(xblock_class) + mixins = filter_mixins_for_standard_xblocks(xblock_class, mixologist=self.mixologist) + xblock_class = Mixologist(mixins).mix(xblock_class) # Make a shallow copy, so that we aren't manipulating a cached field dictionary output_fields = dict(jsonfields) for field_name, value in output_fields.items(): @@ -3030,7 +3034,8 @@ def _serialize_fields(self, category, fields): """ assert isinstance(fields, dict) xblock_class = XBlock.load_class(category, self.default_class) - xblock_class = self.mixologist.mix(xblock_class) + mixins = filter_mixins_for_standard_xblocks(xblock_class, mixologist=self.mixologist) + xblock_class = Mixologist(mixins).mix(xblock_class) def reference_block_id(reference): """ diff --git a/xmodule/modulestore/xml.py b/xmodule/modulestore/xml.py index 679612fde679..3805fad9e2b2 100644 --- a/xmodule/modulestore/xml.py +++ b/xmodule/modulestore/xml.py @@ -26,7 +26,9 @@ ReferenceValueDict, ScopeIds, ) -from xblock.runtime import DictKeyValueStore +from xblock.runtime import DictKeyValueStore, Mixologist + +from openedx.core.djangoapps.xblock.utils import filter_mixins_for_standard_xblocks from common.djangoapps.util.monitoring import monitor_import_failure from xmodule.error_block import ErrorBlock @@ -100,7 +102,9 @@ def xblock_from_node(self, node, parent_id, id_generator=None): usage_id = id_generator.create_usage(def_id) keys = ScopeIds(None, block_type, def_id, usage_id) - block_class = self.mixologist.mix(self.load_block_type(block_type)) + base_class = self.load_block_type(block_type) + mixins = filter_mixins_for_standard_xblocks(base_class, mixologist=self.mixologist) + block_class = Mixologist(mixins).mix(base_class) aside_children = self.parse_asides(node, def_id, usage_id, id_generator) asides_tags = [x.tag for x in aside_children]