Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion cms/djangoapps/contentstore/views/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/views/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions openedx/core/djangoapps/xblock/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import hashlib
import hmac
import inspect
import math
import time
from uuid import uuid4
Expand All @@ -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):
Expand Down Expand Up @@ -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
14 changes: 14 additions & 0 deletions openedx/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion xmodule/modulestore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 5 additions & 2 deletions xmodule/modulestore/mongo/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 7 additions & 2 deletions xmodule/modulestore/split_mongo/split.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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):
"""
Expand Down
8 changes: 6 additions & 2 deletions xmodule/modulestore/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
Loading