refactor: Delete XModule classes (#30262)
Removes XModule, XModuleDescriptor, ProxyAttribute classes and their usages.
This commit is contained in:
@@ -4,8 +4,9 @@ Code to handle mako templating for XModules and XBlocks.
|
||||
|
||||
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.core import XBlock
|
||||
|
||||
from .x_module import DescriptorSystem, XModuleDescriptor, shim_xmodule_js
|
||||
from .x_module import DescriptorSystem, shim_xmodule_js, XModuleMixin
|
||||
|
||||
|
||||
class MakoDescriptorSystem(DescriptorSystem): # lint-amnesty, pylint: disable=abstract-method
|
||||
@@ -71,7 +72,8 @@ class MakoTemplateBlockBase:
|
||||
return fragment
|
||||
|
||||
|
||||
class MakoModuleDescriptor(MakoTemplateBlockBase, XModuleDescriptor): # pylint: disable=abstract-method
|
||||
@XBlock.needs("i18n")
|
||||
class MakoModuleDescriptor(MakoTemplateBlockBase, XModuleMixin): # pylint: disable=abstract-method
|
||||
"""
|
||||
Mixin to use for XModule descriptors.
|
||||
"""
|
||||
|
||||
@@ -51,7 +51,7 @@ from xmodule.modulestore.store_utilities import draft_node_constructor, get_draf
|
||||
from xmodule.modulestore.xml import ImportSystem, LibraryXMLModuleStore, XMLModuleStore
|
||||
from xmodule.tabs import CourseTabList
|
||||
from xmodule.util.misc import escape_invalid_characters
|
||||
from xmodule.x_module import XModuleDescriptor, XModuleMixin
|
||||
from xmodule.x_module import XModuleMixin
|
||||
|
||||
from .inheritance import own_metadata
|
||||
from .store_utilities import rewrite_nonportable_content_links
|
||||
@@ -1346,14 +1346,11 @@ def _update_module_location(module, new_location):
|
||||
"""
|
||||
# Retrieve the content and settings fields that have been explicitly set
|
||||
# to ensure that they are properly re-keyed in the XBlock field data.
|
||||
if isinstance(module, XModuleDescriptor):
|
||||
rekey_fields = []
|
||||
else:
|
||||
rekey_fields = (
|
||||
list(module.get_explicitly_set_fields_by_scope(Scope.content).keys()) +
|
||||
list(module.get_explicitly_set_fields_by_scope(Scope.settings).keys()) +
|
||||
list(module.get_explicitly_set_fields_by_scope(Scope.children).keys())
|
||||
)
|
||||
rekey_fields = (
|
||||
list(module.get_explicitly_set_fields_by_scope(Scope.content).keys()) +
|
||||
list(module.get_explicitly_set_fields_by_scope(Scope.settings).keys()) +
|
||||
list(module.get_explicitly_set_fields_by_scope(Scope.children).keys())
|
||||
)
|
||||
|
||||
module.location = new_location
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from .exceptions import NotFoundError
|
||||
from .fields import Date
|
||||
from .mako_module import MakoTemplateBlockBase
|
||||
from .progress import Progress
|
||||
from .x_module import AUTHOR_VIEW, PUBLIC_VIEW, STUDENT_VIEW, XModule # pylint: disable=unused-import
|
||||
from .x_module import AUTHOR_VIEW, PUBLIC_VIEW, STUDENT_VIEW
|
||||
from .xml_module import XmlMixin
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ from xmodule.seq_module import SequenceBlock
|
||||
from xmodule.split_test_module import SplitTestBlock
|
||||
from xmodule.template_module import CustomTagBlock
|
||||
from xmodule.word_cloud_module import WordCloudBlock
|
||||
from xmodule.x_module import XModuleDescriptor, HTMLSnippet
|
||||
from xmodule.x_module import HTMLSnippet
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -91,44 +91,22 @@ XBLOCK_CLASSES = [
|
||||
|
||||
def write_module_styles(output_root):
|
||||
"""Write all registered XModule css, sass, and scss files to output root."""
|
||||
return _write_styles('.xmodule_display', output_root, _list_modules(), 'get_preview_view_css')
|
||||
return _write_styles('.xmodule_display', output_root, XBLOCK_CLASSES, 'get_preview_view_css')
|
||||
|
||||
|
||||
def write_module_js(output_root):
|
||||
"""Write all registered XModule js and coffee files to output root."""
|
||||
return _write_js(output_root, _list_modules(), 'get_preview_view_js')
|
||||
return _write_js(output_root, XBLOCK_CLASSES, 'get_preview_view_js')
|
||||
|
||||
|
||||
def write_descriptor_styles(output_root):
|
||||
"""Write all registered XModuleDescriptor css, sass, and scss files to output root."""
|
||||
return _write_styles('.xmodule_edit', output_root, _list_descriptors(), 'get_studio_view_css')
|
||||
return _write_styles('.xmodule_edit', output_root, XBLOCK_CLASSES, 'get_studio_view_css')
|
||||
|
||||
|
||||
def write_descriptor_js(output_root):
|
||||
"""Write all registered XModuleDescriptor js and coffee files to output root."""
|
||||
return _write_js(output_root, _list_descriptors(), 'get_studio_view_js')
|
||||
|
||||
|
||||
def _list_descriptors():
|
||||
"""Return a list of all registered XModuleDescriptor classes."""
|
||||
return sorted(
|
||||
[
|
||||
desc for (_, desc) in XModuleDescriptor.load_classes()
|
||||
] + XBLOCK_CLASSES,
|
||||
key=str
|
||||
)
|
||||
|
||||
|
||||
def _list_modules():
|
||||
"""Return a list of all registered XModule classes."""
|
||||
return sorted(
|
||||
[
|
||||
desc.module_class for desc in [
|
||||
desc for (_, desc) in XModuleDescriptor.load_classes()
|
||||
]
|
||||
] + XBLOCK_CLASSES,
|
||||
key=str
|
||||
)
|
||||
return _write_js(output_root, XBLOCK_CLASSES, 'get_studio_view_js')
|
||||
|
||||
|
||||
def _ensure_dir(directory):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Mixin to support editing in Studio.
|
||||
"""
|
||||
from xblock.core import XBlock, XBlockMixin
|
||||
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW, module_attr
|
||||
from xmodule.x_module import AUTHOR_VIEW, STUDENT_VIEW
|
||||
|
||||
|
||||
@XBlock.needs('mako')
|
||||
@@ -51,17 +51,6 @@ class StudioEditableBlock(XBlockMixin):
|
||||
StudioEditableModule = StudioEditableBlock
|
||||
|
||||
|
||||
class StudioEditableDescriptor:
|
||||
"""
|
||||
Helper mixin for supporting Studio editing of xmodules.
|
||||
|
||||
This class is only intended to be used with an XModule Descriptor. This class assumes that the associated
|
||||
XModule will have an "author_view" method for returning an editable preview view of the module.
|
||||
"""
|
||||
author_view = module_attr(AUTHOR_VIEW)
|
||||
has_author_view = True
|
||||
|
||||
|
||||
def has_author_view(descriptor):
|
||||
"""
|
||||
Returns True if the xmodule linked to the descriptor supports "author_view".
|
||||
|
||||
@@ -37,7 +37,7 @@ from xmodule.modulestore.inheritance import InheritanceMixin
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.tests.helpers import StubReplaceURLService, mock_render_template, StubMakoService, StubUserService
|
||||
from xmodule.util.sandboxing import SandboxService
|
||||
from xmodule.x_module import DoNothingCache, ModuleSystem, XModuleDescriptor, XModuleMixin
|
||||
from xmodule.x_module import DoNothingCache, ModuleSystem, XModuleMixin
|
||||
from openedx.core.lib.cache_utils import CacheService
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from opaque_keys.edx.locator import AssetLocator, CourseLocator
|
||||
from path import Path as path
|
||||
|
||||
from xmodule.contentstore.content import ContentStore, StaticContent, StaticContentStream
|
||||
from xmodule.static_content import _list_descriptors, _write_js
|
||||
from xmodule.static_content import XBLOCK_CLASSES, _write_js
|
||||
|
||||
SAMPLE_STRING = """
|
||||
This is a sample string with more than 1024 bytes, the default STREAM_DATA_CHUNK_SIZE
|
||||
@@ -211,7 +211,7 @@ class ContentTest(unittest.TestCase): # lint-amnesty, pylint: disable=missing-c
|
||||
Test that only one filename starts with 000.
|
||||
"""
|
||||
output_root = path('common/static/xmodule/descriptors/js')
|
||||
file_owners = _write_js(output_root, _list_descriptors(), 'get_studio_view_js')
|
||||
file_owners = _write_js(output_root, XBLOCK_CLASSES, 'get_studio_view_js')
|
||||
js_file_paths = {file_path for file_path in sum(list(file_owners.values()), []) if os.path.basename(file_path).startswith('000-')} # lint-amnesty, pylint: disable=line-too-long
|
||||
assert len(js_file_paths) == 1
|
||||
assert 'XModule.Descriptor = (function() {' in open(js_file_paths.pop()).read()
|
||||
|
||||
@@ -4,19 +4,15 @@ Tests for ErrorBlock and NonStaffErrorBlock
|
||||
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.runtime import IdReader, Runtime
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xblock.test.tools import unabc
|
||||
|
||||
from xmodule.error_module import ErrorBlock, NonStaffErrorBlock
|
||||
from xmodule.modulestore.xml import CourseLocationManager
|
||||
from xmodule.tests import get_test_system
|
||||
from xmodule.x_module import STUDENT_VIEW, XModule, XModuleDescriptor
|
||||
from xmodule.x_module import STUDENT_VIEW
|
||||
|
||||
|
||||
class SetupTestErrorBlock(unittest.TestCase):
|
||||
@@ -49,21 +45,6 @@ class TestErrorBlock(SetupTestErrorBlock):
|
||||
assert self.error_msg in context_repr
|
||||
assert repr(self.valid_xml) in context_repr
|
||||
|
||||
def test_error_block_from_descriptor(self):
|
||||
descriptor = MagicMock(
|
||||
spec=XModuleDescriptor,
|
||||
runtime=self.system,
|
||||
location=self.location,
|
||||
)
|
||||
|
||||
error_descriptor = ErrorBlock.from_descriptor(
|
||||
descriptor, self.error_msg)
|
||||
assert isinstance(error_descriptor, ErrorBlock)
|
||||
error_descriptor.xmodule_runtime = self.system
|
||||
context_repr = self.system.render(error_descriptor, STUDENT_VIEW).content
|
||||
assert self.error_msg in context_repr
|
||||
assert repr(descriptor) in context_repr
|
||||
|
||||
|
||||
class TestNonStaffErrorBlock(SetupTestErrorBlock):
|
||||
"""
|
||||
@@ -88,82 +69,3 @@ class TestNonStaffErrorBlock(SetupTestErrorBlock):
|
||||
context_repr = self.system.render(descriptor, STUDENT_VIEW).content
|
||||
assert self.error_msg not in context_repr
|
||||
assert repr(self.valid_xml) not in context_repr
|
||||
|
||||
def test_error_block_from_descriptor(self):
|
||||
descriptor = MagicMock(
|
||||
spec=XModuleDescriptor,
|
||||
runtime=self.system,
|
||||
location=self.location,
|
||||
)
|
||||
|
||||
error_descriptor = NonStaffErrorBlock.from_descriptor(
|
||||
descriptor, self.error_msg)
|
||||
assert isinstance(error_descriptor, ErrorBlock)
|
||||
error_descriptor.xmodule_runtime = self.system
|
||||
context_repr = self.system.render(error_descriptor, STUDENT_VIEW).content
|
||||
assert self.error_msg not in context_repr
|
||||
assert str(descriptor) not in context_repr
|
||||
|
||||
|
||||
class BrokenModule(XModule): # lint-amnesty, pylint: disable=abstract-method
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
raise Exception("This is a broken xmodule")
|
||||
|
||||
|
||||
class BrokenDescriptor(XModuleDescriptor): # lint-amnesty, pylint: disable=abstract-method
|
||||
module_class = BrokenModule
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
"""An exception type to use to verify raises in tests"""
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
@unabc("Tests should not call {}")
|
||||
class TestRuntime(Runtime): # lint-amnesty, pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
|
||||
class TestErrorBlockConstruction(unittest.TestCase):
|
||||
"""
|
||||
Test that error block construction happens correctly
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
# pylint: disable=abstract-class-instantiated
|
||||
super().setUp()
|
||||
field_data = DictFieldData({})
|
||||
self.descriptor = BrokenDescriptor(
|
||||
TestRuntime(Mock(spec=IdReader), field_data),
|
||||
field_data,
|
||||
ScopeIds(None, None, None,
|
||||
BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'broken', 'name'))
|
||||
)
|
||||
self.descriptor.xmodule_runtime = TestRuntime(Mock(spec=IdReader), field_data)
|
||||
self.descriptor.xmodule_runtime.error_descriptor_class = ErrorBlock
|
||||
self.descriptor.xmodule_runtime.xmodule_instance = None
|
||||
|
||||
def test_broken_block(self):
|
||||
"""
|
||||
Test that when an XModule throws an block during __init__, we
|
||||
get an ErrorBlock back from XModuleDescriptor._xmodule
|
||||
"""
|
||||
module = self.descriptor._xmodule # lint-amnesty, pylint: disable=protected-access
|
||||
assert isinstance(module, ErrorBlock)
|
||||
|
||||
@patch.object(ErrorBlock, '__init__', Mock(side_effect=TestException))
|
||||
def test_broken_error_descriptor(self):
|
||||
"""
|
||||
Test that a broken block descriptor doesn't cause an infinite loop
|
||||
"""
|
||||
with pytest.raises(TestException):
|
||||
module = self.descriptor._xmodule # lint-amnesty, pylint: disable=protected-access, unused-variable
|
||||
|
||||
@patch.object(ErrorBlock, '__init__', Mock(side_effect=TestException))
|
||||
def test_broken_error_block(self):
|
||||
"""
|
||||
Test that a broken block module doesn't cause an infinite loop
|
||||
"""
|
||||
with pytest.raises(TestException):
|
||||
module = self.descriptor._xmodule # lint-amnesty, pylint: disable=protected-access, unused-variable
|
||||
|
||||
@@ -2,15 +2,9 @@
|
||||
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from xblock.field_data import DictFieldData
|
||||
|
||||
from xmodule import x_module
|
||||
from xmodule.progress import Progress
|
||||
|
||||
from . import get_test_system
|
||||
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
@@ -113,14 +107,3 @@ class ProgressTest(unittest.TestCase):
|
||||
# Check != while we're at it
|
||||
assert prg1 != prg2
|
||||
assert prg1 == prg3
|
||||
|
||||
|
||||
class ModuleProgressTest(unittest.TestCase):
|
||||
''' Test that get_progress() does the right thing for the different modules
|
||||
'''
|
||||
|
||||
def test_xmodule_default(self):
|
||||
'''Make sure default get_progress exists, returns None'''
|
||||
xmod = x_module.XModule(Mock(), get_test_system(), DictFieldData({'location': 'a://b/c/d/e'}), Mock())
|
||||
prg = xmod.get_progress() # lint-amnesty, pylint: disable=assignment-from-none
|
||||
assert prg is None
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
"""
|
||||
Tests for the wrapping layer that provides the XBlock API using XModule/Descriptor
|
||||
functionality
|
||||
"""
|
||||
# For tests, ignore access to protected members
|
||||
# pylint: disable=protected-access
|
||||
|
||||
|
||||
from unittest.case import SkipTest, TestCase
|
||||
from unittest.mock import Mock
|
||||
|
||||
import ddt
|
||||
import webob
|
||||
from webob.multidict import MultiDict
|
||||
from factory import (
|
||||
BUILD_STRATEGY,
|
||||
Factory,
|
||||
LazyAttributeSequence,
|
||||
SubFactory,
|
||||
lazy_attribute,
|
||||
post_generation,
|
||||
use_strategy
|
||||
)
|
||||
from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
||||
from xblock.core import XBlock
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from xmodule.annotatable_module import AnnotatableBlock
|
||||
from xmodule.conditional_module import ConditionalBlock
|
||||
from xmodule.course_module import CourseBlock
|
||||
from xmodule.html_module import HtmlBlock
|
||||
from xmodule.poll_module import PollBlock
|
||||
from xmodule.randomize_module import RandomizeBlock
|
||||
from xmodule.seq_module import SequenceBlock
|
||||
from xmodule.tests import get_test_descriptor_system, get_test_system
|
||||
from xmodule.vertical_block import VerticalBlock
|
||||
from xmodule.word_cloud_module import WordCloudBlock
|
||||
from xmodule.wrapper_module import WrapperBlock
|
||||
from xmodule.x_module import (
|
||||
PUBLIC_VIEW,
|
||||
STUDENT_VIEW,
|
||||
STUDIO_VIEW,
|
||||
DescriptorSystem,
|
||||
ModuleSystem,
|
||||
XModule,
|
||||
XModuleDescriptor
|
||||
)
|
||||
|
||||
# A dictionary that maps specific XModuleDescriptor classes without children
|
||||
# to a list of sample field values to test with.
|
||||
# TODO: Add more types of sample data
|
||||
LEAF_XMODULES = {
|
||||
AnnotatableBlock: [{}],
|
||||
HtmlBlock: [{}],
|
||||
PollBlock: [{'display_name': 'Poll Display Name'}],
|
||||
WordCloudBlock: [{}],
|
||||
}
|
||||
|
||||
|
||||
# A dictionary that maps specific XModuleDescriptor classes with children
|
||||
# to a list of sample field values to test with.
|
||||
# TODO: Add more types of sample data
|
||||
CONTAINER_XMODULES = {
|
||||
ConditionalBlock: [{}],
|
||||
CourseBlock: [{}],
|
||||
RandomizeBlock: [{'display_name': 'Test String Display'}],
|
||||
SequenceBlock: [{'display_name': 'Test Unicode हिंदी Display'}],
|
||||
VerticalBlock: [{}],
|
||||
WrapperBlock: [{}],
|
||||
}
|
||||
|
||||
# These modules are not editable in studio yet
|
||||
NOT_STUDIO_EDITABLE = (
|
||||
PollBlock,
|
||||
)
|
||||
|
||||
|
||||
def flatten(class_dict):
|
||||
"""
|
||||
Flatten a dict from cls -> [fields, ...] and yields values of the form (cls, fields)
|
||||
for each entry in the dictionary value.
|
||||
"""
|
||||
for cls in sorted(class_dict, key=lambda err: err.__name__):
|
||||
fields_list = class_dict[cls]
|
||||
for fields in fields_list:
|
||||
yield (cls, fields)
|
||||
|
||||
|
||||
@use_strategy(BUILD_STRATEGY)
|
||||
class ModuleSystemFactory(Factory):
|
||||
"""
|
||||
Factory to build a test ModuleSystem. Creation is
|
||||
performed by :func:`xmodule.tests.get_test_system`, so
|
||||
arguments for that function are valid factory attributes.
|
||||
"""
|
||||
class Meta:
|
||||
model = ModuleSystem
|
||||
|
||||
@classmethod
|
||||
def _build(cls, target_class, *args, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, unused-argument
|
||||
"""See documentation from :meth:`factory.Factory._build`"""
|
||||
return get_test_system(*args, **kwargs)
|
||||
|
||||
|
||||
@use_strategy(BUILD_STRATEGY)
|
||||
class DescriptorSystemFactory(Factory):
|
||||
"""
|
||||
Factory to build a test DescriptorSystem. Creation is
|
||||
performed by :func:`xmodule.tests.get_test_descriptor_system`, so
|
||||
arguments for that function are valid factory attributes.
|
||||
"""
|
||||
class Meta:
|
||||
model = DescriptorSystem
|
||||
|
||||
@classmethod
|
||||
def _build(cls, target_class, *args, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, unused-argument
|
||||
"""See documentation from :meth:`factory.Factory._build`"""
|
||||
return get_test_descriptor_system(*args, **kwargs)
|
||||
|
||||
|
||||
class ContainerModuleRuntimeFactory(ModuleSystemFactory):
|
||||
"""
|
||||
Factory to generate a ModuleRuntime that generates children when asked
|
||||
for them, for testing container XModules.
|
||||
"""
|
||||
@post_generation
|
||||
def depth(self, create, depth, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
When `depth` is specified as a Factory parameter, creates a
|
||||
tree of children with that many levels.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
if depth == 0:
|
||||
self.get_module.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlBlock)
|
||||
else:
|
||||
self.get_module.side_effect = lambda x: ContainerModuleFactory(
|
||||
descriptor_cls=VerticalBlock,
|
||||
depth=depth - 1
|
||||
)
|
||||
|
||||
@post_generation
|
||||
def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden
|
||||
"""
|
||||
Update the position attribute of the generated ModuleRuntime.
|
||||
"""
|
||||
self.position = position
|
||||
|
||||
|
||||
class ContainerDescriptorRuntimeFactory(DescriptorSystemFactory):
|
||||
"""
|
||||
Factory to generate a DescriptorRuntime that generates children when asked
|
||||
for them, for testing container XModuleDescriptors.
|
||||
"""
|
||||
@post_generation
|
||||
def depth(self, create, depth, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
When `depth` is specified as a Factory parameter, creates a
|
||||
tree of children with that many levels.
|
||||
"""
|
||||
# pylint: disable=no-member
|
||||
if depth == 0:
|
||||
self.load_item.side_effect = lambda x: LeafModuleFactory(descriptor_cls=HtmlBlock)
|
||||
else:
|
||||
self.load_item.side_effect = lambda x: ContainerModuleFactory(
|
||||
descriptor_cls=VerticalBlock,
|
||||
depth=depth - 1
|
||||
)
|
||||
|
||||
@post_generation
|
||||
def position(self, create, position=2, **kwargs): # pylint: disable=unused-argument, method-hidden
|
||||
"""
|
||||
Update the position attribute of the generated ModuleRuntime.
|
||||
"""
|
||||
self.position = position
|
||||
|
||||
|
||||
@use_strategy(BUILD_STRATEGY)
|
||||
class LeafDescriptorFactory(Factory):
|
||||
"""
|
||||
Factory to generate leaf XModuleDescriptors.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = XModuleDescriptor
|
||||
|
||||
runtime = SubFactory(DescriptorSystemFactory)
|
||||
url_name = LazyAttributeSequence('{.block_type}_{}'.format)
|
||||
|
||||
@lazy_attribute
|
||||
def location(self):
|
||||
return BlockUsageLocator(CourseLocator('org', 'course', 'run'), 'category', self.url_name)
|
||||
|
||||
@lazy_attribute
|
||||
def block_type(self):
|
||||
return self.descriptor_cls.__name__ # pylint: disable=no-member
|
||||
|
||||
@lazy_attribute
|
||||
def definition_id(self):
|
||||
return self.location
|
||||
|
||||
@lazy_attribute
|
||||
def usage_id(self):
|
||||
return self.location
|
||||
|
||||
@classmethod
|
||||
def _build(cls, target_class, *args, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, unused-argument
|
||||
runtime = kwargs.pop('runtime')
|
||||
desc_cls = kwargs.pop('descriptor_cls')
|
||||
block_type = kwargs.pop('block_type')
|
||||
def_id = kwargs.pop('definition_id')
|
||||
usage_id = kwargs.pop('usage_id')
|
||||
|
||||
block = runtime.construct_xblock_from_class(
|
||||
desc_cls,
|
||||
ScopeIds(None, block_type, def_id, usage_id),
|
||||
DictFieldData(dict(**kwargs))
|
||||
)
|
||||
block.save()
|
||||
return block
|
||||
|
||||
|
||||
class LeafModuleFactory(LeafDescriptorFactory):
|
||||
"""
|
||||
Factory to generate leaf XModuleDescriptors that are prepped to be
|
||||
used as XModules.
|
||||
"""
|
||||
@post_generation
|
||||
def xmodule_runtime(self, create, xmodule_runtime, **kwargs): # pylint: disable=method-hidden, unused-argument
|
||||
"""
|
||||
Set the xmodule_runtime to make this XModuleDescriptor usable
|
||||
as an XModule.
|
||||
"""
|
||||
if xmodule_runtime is None:
|
||||
xmodule_runtime = ModuleSystemFactory()
|
||||
|
||||
self.xmodule_runtime = xmodule_runtime
|
||||
|
||||
|
||||
class ContainerDescriptorFactory(LeafDescriptorFactory):
|
||||
"""
|
||||
Factory to generate XModuleDescriptors that are containers.
|
||||
"""
|
||||
runtime = SubFactory(ContainerDescriptorRuntimeFactory)
|
||||
children = list(range(3))
|
||||
|
||||
|
||||
class ContainerModuleFactory(LeafModuleFactory):
|
||||
"""
|
||||
Factory to generate XModuleDescriptors that are containers
|
||||
and are ready to act as XModules.
|
||||
"""
|
||||
@lazy_attribute
|
||||
def xmodule_runtime(self): # lint-amnesty, pylint: disable=arguments-differ
|
||||
return ContainerModuleRuntimeFactory(depth=self.depth) # pylint: disable=no-member
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class XBlockWrapperTestMixin:
|
||||
"""
|
||||
This is a mixin for building tests of the implementation of the XBlock
|
||||
api by wrapping XModule native functions.
|
||||
|
||||
You can create an actual test case by inheriting from this class and UnitTest,
|
||||
and implement skip_if_invalid and check_property.
|
||||
"""
|
||||
|
||||
def skip_if_invalid(self, descriptor_cls):
|
||||
"""
|
||||
Raise SkipTest if this descriptor_cls shouldn't be tested.
|
||||
"""
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
def check_property(self, descriptor):
|
||||
"""
|
||||
Execute assertions to verify that the property under test is true for
|
||||
the supplied descriptor.
|
||||
"""
|
||||
raise SkipTest("check_property not defined")
|
||||
|
||||
# Test that for all of the leaf XModule Descriptors,
|
||||
# the test property holds
|
||||
@ddt.data(*flatten(LEAF_XMODULES))
|
||||
def test_leaf_node(self, cls_and_fields):
|
||||
descriptor_cls, fields = cls_and_fields
|
||||
self.skip_if_invalid(descriptor_cls)
|
||||
descriptor = LeafModuleFactory(descriptor_cls=descriptor_cls, **fields)
|
||||
mocked_course = Mock()
|
||||
modulestore = Mock()
|
||||
modulestore.get_course.return_value = mocked_course
|
||||
# pylint: disable=no-member
|
||||
descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a')
|
||||
descriptor.runtime.modulestore = modulestore
|
||||
if hasattr(descriptor, '_xmodule'):
|
||||
descriptor._xmodule.graded = 'False'
|
||||
self.check_property(descriptor)
|
||||
|
||||
# Test that when an xmodule is generated from descriptor_cls
|
||||
# with only xmodule children, the test property holds
|
||||
@ddt.data(*flatten(CONTAINER_XMODULES))
|
||||
def test_container_node_xmodules_only(self, cls_and_fields):
|
||||
descriptor_cls, fields = cls_and_fields
|
||||
self.skip_if_invalid(descriptor_cls)
|
||||
descriptor = ContainerModuleFactory(descriptor_cls=descriptor_cls, depth=2, **fields)
|
||||
descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a')
|
||||
self.check_property(descriptor)
|
||||
|
||||
# Test that when an xmodule is generated from descriptor_cls
|
||||
# with mixed xmodule and xblock children, the test property holds
|
||||
@ddt.data(*flatten(CONTAINER_XMODULES))
|
||||
def test_container_node_mixed(self, cls_and_fields):
|
||||
raise SkipTest("XBlock support in XDescriptor not yet fully implemented")
|
||||
|
||||
# Test that when an xmodule is generated from descriptor_cls
|
||||
# with only xblock children, the test property holds
|
||||
@ddt.data(*flatten(CONTAINER_XMODULES))
|
||||
def test_container_node_xblocks_only(self, cls_and_fields):
|
||||
raise SkipTest("XBlock support in XModules not yet fully implemented")
|
||||
|
||||
|
||||
class TestStudentView(XBlockWrapperTestMixin, TestCase):
|
||||
"""
|
||||
This tests that student_view and XModule.get_html produce the same results.
|
||||
"""
|
||||
|
||||
def skip_if_invalid(self, descriptor_cls):
|
||||
pure_xblock_class = issubclass(descriptor_cls, XBlock) and not issubclass(descriptor_cls, XModuleDescriptor)
|
||||
if pure_xblock_class:
|
||||
student_view = descriptor_cls.student_view
|
||||
else:
|
||||
student_view = descriptor_cls.module_class.student_view
|
||||
if student_view != XModule.student_view:
|
||||
raise SkipTest(descriptor_cls.__name__ + " implements student_view")
|
||||
|
||||
def check_property(self, descriptor):
|
||||
"""
|
||||
Assert that both student_view and get_html render the same.
|
||||
"""
|
||||
assert descriptor._xmodule.get_html() == descriptor.render(STUDENT_VIEW).content
|
||||
|
||||
|
||||
class TestStudioView(XBlockWrapperTestMixin, TestCase):
|
||||
"""
|
||||
This tests that studio_view and XModuleDescriptor.get_html produce the same results
|
||||
"""
|
||||
|
||||
def skip_if_invalid(self, descriptor_cls):
|
||||
if descriptor_cls in NOT_STUDIO_EDITABLE:
|
||||
raise SkipTest(descriptor_cls.__name__ + " is not editable in studio")
|
||||
|
||||
pure_xblock_class = issubclass(descriptor_cls, XBlock) and not issubclass(descriptor_cls, XModuleDescriptor)
|
||||
if pure_xblock_class: # lint-amnesty, pylint: disable=no-else-raise
|
||||
raise SkipTest(descriptor_cls.__name__ + " is a pure XBlock and implements studio_view")
|
||||
elif descriptor_cls.studio_view != XModuleDescriptor.studio_view:
|
||||
raise SkipTest(descriptor_cls.__name__ + " implements studio_view")
|
||||
|
||||
def check_property(self, descriptor):
|
||||
"""
|
||||
Assert that studio_view and get_html render the same.
|
||||
"""
|
||||
html = descriptor.get_html()
|
||||
rendered_content = descriptor.render(STUDIO_VIEW).content
|
||||
assert html == rendered_content
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestXModuleHandler(TestCase):
|
||||
"""
|
||||
Tests that the xmodule_handler function correctly wraps handle_ajax
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.module = XModule(descriptor=Mock(), field_data=Mock(), runtime=Mock(), scope_ids=Mock())
|
||||
self.module.handle_ajax = Mock(return_value='{}')
|
||||
self.request = webob.Request({})
|
||||
|
||||
def test_xmodule_handler_passed_data(self):
|
||||
self.module.xmodule_handler(self.request)
|
||||
self.module.handle_ajax.assert_called_with(None, MultiDict(self.request.POST))
|
||||
|
||||
def test_xmodule_handler_dispatch(self):
|
||||
self.module.xmodule_handler(self.request, 'dispatch')
|
||||
self.module.handle_ajax.assert_called_with('dispatch', MultiDict(self.request.POST))
|
||||
|
||||
def test_xmodule_handler_return_value(self):
|
||||
response = self.module.xmodule_handler(self.request)
|
||||
assert isinstance(response, webob.Response)
|
||||
assert response.body.decode('utf-8') == '{}'
|
||||
|
||||
@ddt.data(
|
||||
'{"test_key": "test_value"}',
|
||||
'{"test_key": "test_value"}',
|
||||
)
|
||||
def test_xmodule_handler_with_data(self, response_data):
|
||||
"""
|
||||
Tests that xmodule_handler function correctly wraps handle_ajax when handle_ajax response is either
|
||||
str or unicode.
|
||||
"""
|
||||
|
||||
self.module.handle_ajax = Mock(return_value=response_data)
|
||||
response = self.module.xmodule_handler(self.request)
|
||||
assert isinstance(response, webob.Response)
|
||||
assert response.body.decode('utf-8') == '{"test_key": "test_value"}'
|
||||
|
||||
|
||||
class TestXmlExport(XBlockWrapperTestMixin, TestCase):
|
||||
"""
|
||||
This tests that XModuleDescriptor.export_course_to_xml and add_xml_to_node produce the same results.
|
||||
"""
|
||||
|
||||
def skip_if_invalid(self, descriptor_cls):
|
||||
if descriptor_cls.add_xml_to_node != XModuleDescriptor.add_xml_to_node:
|
||||
raise SkipTest(descriptor_cls.__name__ + " implements add_xml_to_node")
|
||||
|
||||
def check_property(self, descriptor):
|
||||
xmodule_api_fs = MemoryFS()
|
||||
xblock_api_fs = MemoryFS()
|
||||
|
||||
descriptor.runtime.export_fs = xblock_api_fs
|
||||
xblock_node = etree.Element('unknown')
|
||||
descriptor.add_xml_to_node(xblock_node)
|
||||
|
||||
xmodule_node = etree.fromstring(descriptor.export_to_xml(xmodule_api_fs))
|
||||
|
||||
assert list(xmodule_api_fs.walk()) == list(xblock_api_fs.walk())
|
||||
assert etree.tostring(xmodule_node) == etree.tostring(xblock_node)
|
||||
|
||||
|
||||
class TestPublicView(XBlockWrapperTestMixin, TestCase):
|
||||
"""
|
||||
This tests that default public_view shows the correct message.
|
||||
"""
|
||||
|
||||
def skip_if_invalid(self, descriptor_cls):
|
||||
pure_xblock_class = issubclass(descriptor_cls, XBlock) and not issubclass(descriptor_cls, XModuleDescriptor)
|
||||
if pure_xblock_class:
|
||||
public_view = descriptor_cls.public_view
|
||||
else:
|
||||
public_view = descriptor_cls.module_class.public_view
|
||||
if public_view != XModule.public_view:
|
||||
raise SkipTest(descriptor_cls.__name__ + " implements public_view")
|
||||
|
||||
def check_property(self, descriptor):
|
||||
"""
|
||||
Assert that public_view contains correct message.
|
||||
"""
|
||||
if descriptor.display_name:
|
||||
assert descriptor.display_name in descriptor.render(PUBLIC_VIEW).content
|
||||
else:
|
||||
assert 'This content is only accessible' in descriptor.render(PUBLIC_VIEW).content
|
||||
@@ -251,26 +251,18 @@ class HTMLSnippet:
|
||||
|
||||
@classmethod
|
||||
def get_preview_view_js(cls):
|
||||
if issubclass(cls, XModule):
|
||||
return cls.get_javascript()
|
||||
return cls.preview_view_js
|
||||
|
||||
@classmethod
|
||||
def get_preview_view_js_bundle_name(cls):
|
||||
if issubclass(cls, XModule):
|
||||
return cls.__name__
|
||||
return cls.__name__ + 'Preview'
|
||||
|
||||
@classmethod
|
||||
def get_studio_view_js(cls):
|
||||
if issubclass(cls, XModuleDescriptor):
|
||||
return cls.get_javascript()
|
||||
return cls.studio_view_js
|
||||
|
||||
@classmethod
|
||||
def get_studio_view_js_bundle_name(cls):
|
||||
if issubclass(cls, XModuleDescriptor):
|
||||
return cls.__name__
|
||||
return cls.__name__ + 'Studio'
|
||||
|
||||
@classmethod
|
||||
@@ -291,14 +283,10 @@ class HTMLSnippet:
|
||||
|
||||
@classmethod
|
||||
def get_preview_view_css(cls):
|
||||
if issubclass(cls, XModule):
|
||||
return cls.get_css()
|
||||
return cls.preview_view_css
|
||||
|
||||
@classmethod
|
||||
def get_studio_view_css(cls):
|
||||
if issubclass(cls, XModuleDescriptor):
|
||||
return cls.get_css()
|
||||
return cls.studio_view_css
|
||||
|
||||
def get_html(self):
|
||||
@@ -834,54 +822,6 @@ class XModuleMixin(XModuleFields, XBlock):
|
||||
return Fragment(alert_html.format(display_text))
|
||||
|
||||
|
||||
class ProxyAttribute:
|
||||
"""
|
||||
A (python) descriptor that proxies attribute access.
|
||||
|
||||
For example:
|
||||
|
||||
class Foo(object):
|
||||
def __init__(self, value):
|
||||
self.foo_attr = value
|
||||
|
||||
class Bar(object):
|
||||
foo = Foo('x')
|
||||
foo_attr = ProxyAttribute('foo', 'foo_attr')
|
||||
|
||||
bar = Bar()
|
||||
|
||||
assert bar.foo_attr == 'x'
|
||||
bar.foo_attr = 'y'
|
||||
assert bar.foo.foo_attr == 'y'
|
||||
del bar.foo_attr
|
||||
assert not hasattr(bar.foo, 'foo_attr')
|
||||
"""
|
||||
def __init__(self, source, name):
|
||||
"""
|
||||
:param source: The name of the attribute to proxy to
|
||||
:param name: The name of the attribute to proxy
|
||||
"""
|
||||
self._source = source
|
||||
self._name = name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
|
||||
return getattr(getattr(instance, self._source), self._name)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
setattr(getattr(instance, self._source), self._name, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
delattr(getattr(instance, self._source), self._name)
|
||||
|
||||
|
||||
module_attr = partial(ProxyAttribute, '_xmodule') # pylint: disable=invalid-name
|
||||
descriptor_attr = partial(ProxyAttribute, 'descriptor') # pylint: disable=invalid-name
|
||||
module_runtime_attr = partial(ProxyAttribute, 'xmodule_runtime') # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class XModuleToXBlockMixin:
|
||||
"""
|
||||
Common code needed by XModule and XBlocks converted from XModules.
|
||||
@@ -926,108 +866,6 @@ class XModuleToXBlockMixin:
|
||||
return Response(response_data, content_type='application/json', charset='UTF-8')
|
||||
|
||||
|
||||
@XBlock.needs("i18n")
|
||||
class XModule(XModuleToXBlockMixin, HTMLSnippet, XModuleMixin): # lint-amnesty, pylint: disable=abstract-method
|
||||
""" Implements a generic learning module.
|
||||
|
||||
Subclasses must at a minimum provide a definition for get_html in order
|
||||
to be displayed to users.
|
||||
|
||||
See the HTML module for a simple example.
|
||||
"""
|
||||
|
||||
entry_point = "xmodule.v1"
|
||||
|
||||
has_score = descriptor_attr('has_score')
|
||||
max_score = descriptor_attr('max_score')
|
||||
show_in_read_only_mode = descriptor_attr('show_in_read_only_mode')
|
||||
_field_data_cache = descriptor_attr('_field_data_cache')
|
||||
_field_data = descriptor_attr('_field_data')
|
||||
_dirty_fields = descriptor_attr('_dirty_fields')
|
||||
|
||||
def __init__(self, descriptor, *args, **kwargs):
|
||||
"""
|
||||
Construct a new xmodule
|
||||
|
||||
runtime: An XBlock runtime allowing access to external resources
|
||||
|
||||
descriptor: the XModuleDescriptor that this module is an instance of.
|
||||
|
||||
field_data: A dictionary-like object that maps field names to values
|
||||
for those fields.
|
||||
"""
|
||||
|
||||
# Set the descriptor first so that we can proxy to it
|
||||
self.descriptor = descriptor
|
||||
self._runtime = None
|
||||
super().__init__(*args, **kwargs)
|
||||
self.runtime.xmodule_instance = self
|
||||
|
||||
@property
|
||||
def runtime(self):
|
||||
return CombinedSystem(self._runtime, self.descriptor._runtime) # pylint: disable=protected-access
|
||||
|
||||
@runtime.setter
|
||||
def runtime(self, value):
|
||||
self._runtime = value
|
||||
|
||||
def __str__(self):
|
||||
# xss-lint: disable=python-wrap-html
|
||||
return f'<x_module(id={self.id})>' # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
def handle_ajax(self, _dispatch, _data):
|
||||
""" dispatch is last part of the URL.
|
||||
data is a dictionary-like object with the content of the request"""
|
||||
return ""
|
||||
|
||||
def get_child(self, usage_id):
|
||||
if usage_id in self._child_cache:
|
||||
return self._child_cache[usage_id]
|
||||
|
||||
# Take advantage of the children cache that the descriptor might have
|
||||
child_descriptor = self.descriptor.get_child(usage_id)
|
||||
child_block = None
|
||||
if child_descriptor is not None:
|
||||
child_block = self.system.get_module(child_descriptor)
|
||||
|
||||
self._child_cache[usage_id] = child_block
|
||||
return child_block
|
||||
|
||||
def get_child_descriptors(self):
|
||||
"""
|
||||
Returns the descriptors of the child modules
|
||||
|
||||
Overriding this changes the behavior of get_children and
|
||||
anything that uses get_children, such as get_display_items.
|
||||
|
||||
This method will not instantiate the modules of the children
|
||||
unless absolutely necessary, so it is cheaper to call than get_children
|
||||
|
||||
These children will be the same children returned by the
|
||||
descriptor unless descriptor.has_dynamic_children() is true.
|
||||
"""
|
||||
return self.descriptor.get_children()
|
||||
|
||||
def displayable_items(self):
|
||||
"""
|
||||
Returns list of displayable modules contained by this module. If this
|
||||
module is visible, should return [self].
|
||||
"""
|
||||
return [self.descriptor]
|
||||
|
||||
# ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~
|
||||
def student_view(self, context): # lint-amnesty, pylint: disable=unused-argument
|
||||
"""
|
||||
Return a fragment with the html from this XModule
|
||||
|
||||
Doesn't yet add any of the javascript to the fragment, nor the css.
|
||||
Also doesn't expect any javascript binding, yet.
|
||||
|
||||
Makes no use of the context parameter
|
||||
"""
|
||||
return Fragment(self.get_html())
|
||||
|
||||
|
||||
def policy_key(location):
|
||||
"""
|
||||
Get the key for a location in a policy file. (Since the policy file is
|
||||
@@ -1104,161 +942,6 @@ class ResourceTemplates:
|
||||
return template
|
||||
|
||||
|
||||
@XBlock.needs("i18n")
|
||||
class XModuleDescriptor(HTMLSnippet, ResourceTemplates, XModuleMixin): # lint-amnesty, pylint: disable=abstract-method
|
||||
"""
|
||||
An XModuleDescriptor is a specification for an element of a course. This
|
||||
could be a problem, an organizational element (a group of content), or a
|
||||
segment of video, for example.
|
||||
|
||||
XModuleDescriptors are independent and agnostic to the current student state
|
||||
on a problem. They handle the editing interface used by instructors to
|
||||
create a problem, and can generate XModules (which do know about student
|
||||
state).
|
||||
"""
|
||||
|
||||
entry_point = "xmodule.v1"
|
||||
|
||||
module_class = XModule
|
||||
|
||||
# ============================= STRUCTURAL MANIPULATION ===================
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Construct a new XModuleDescriptor. The only required arguments are the
|
||||
system, used for interaction with external resources, and the
|
||||
definition, which specifies all the data needed to edit and display the
|
||||
problem (but none of the associated metadata that handles recordkeeping
|
||||
around the problem).
|
||||
|
||||
This allows for maximal flexibility to add to the interface while
|
||||
preserving backwards compatibility.
|
||||
|
||||
runtime: A DescriptorSystem for interacting with external resources
|
||||
|
||||
field_data: A dictionary-like object that maps field names to values
|
||||
for those fields.
|
||||
|
||||
XModuleDescriptor.__init__ takes the same arguments as xblock.core:XBlock.__init__
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
# update_version is the version which last updated this xblock v prev being the penultimate updater
|
||||
# leaving off original_version since it complicates creation w/o any obv value yet and is computable
|
||||
# by following previous until None
|
||||
# definition_locator is only used by mongostores which separate definitions from blocks
|
||||
self.previous_version = self.update_version = self.definition_locator = None
|
||||
self.xmodule_runtime = None
|
||||
|
||||
def editor_saved(self, user, old_metadata, old_content):
|
||||
"""
|
||||
This method is called when "Save" is pressed on the Studio editor.
|
||||
|
||||
Note that after this method is called, the modulestore update_item method will
|
||||
be called on this xmodule. Therefore, any modifications to the xmodule that are
|
||||
performed in editor_saved will automatically be persisted (calling update_item
|
||||
from implementors of this method is not necessary).
|
||||
|
||||
Args:
|
||||
user: the user who requested the save (as obtained from the request)
|
||||
old_metadata (dict): the values of the fields with Scope.settings before the save was performed
|
||||
old_content (dict): the values of the fields with Scope.content before the save was performed.
|
||||
This will include 'data'.
|
||||
"""
|
||||
pass # lint-amnesty, pylint: disable=unnecessary-pass
|
||||
|
||||
# =============================== BUILTIN METHODS ==========================
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Is this XModule effectively equal to the other instance?
|
||||
"""
|
||||
return (hasattr(other, 'scope_ids') and
|
||||
self.scope_ids == other.scope_ids and
|
||||
set(self.fields.keys()) == set(other.fields.keys()) and # lint-amnesty, pylint: disable=no-member
|
||||
all(getattr(self, field.name) == getattr(other, field.name)
|
||||
for field in self.fields.values())) # lint-amnesty, pylint: disable=no-member
|
||||
|
||||
def __hash__(self): # pylint: disable=useless-super-delegation
|
||||
"""
|
||||
This isn't technically appropriate since descriptors are actually mutable,
|
||||
but in practice we rarely modify them after creation or instantiate two
|
||||
equivalent descriptors in the same process. And we perform graph
|
||||
operations on large collections of XBlocks that have simply unacceptable
|
||||
performance if we have to rely on lists and equality rather than sets,
|
||||
dictionaries, and identity-based hash functions.
|
||||
"""
|
||||
return super().__hash__()
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
"{0.__class__.__name__}("
|
||||
"{0.runtime!r}, "
|
||||
"{0._field_data!r}, "
|
||||
"{0.scope_ids!r}"
|
||||
")".format(self)
|
||||
)
|
||||
|
||||
# ~~~~~~~~~~~~~~~ XModule Indirection ~~~~~~~~~~~~~~~~
|
||||
@property
|
||||
def _xmodule(self):
|
||||
"""
|
||||
Returns the XModule corresponding to this descriptor. Expects that the system
|
||||
already supports all of the attributes needed by xmodules
|
||||
"""
|
||||
if self.xmodule_runtime is None:
|
||||
raise UndefinedContext()
|
||||
assert self.xmodule_runtime.error_descriptor_class is not None
|
||||
if self.xmodule_runtime.xmodule_instance is None:
|
||||
try:
|
||||
self.xmodule_runtime.construct_xblock_from_class(
|
||||
self.module_class,
|
||||
descriptor=self,
|
||||
scope_ids=self.scope_ids,
|
||||
field_data=self._field_data,
|
||||
for_parent=self.get_parent() if self.has_cached_parent else None
|
||||
)
|
||||
self.xmodule_runtime.xmodule_instance.save()
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# xmodule_instance is set by the XModule.__init__. If we had an error after that,
|
||||
# we need to clean it out so that we can set up the ErrorBlock instead
|
||||
self.xmodule_runtime.xmodule_instance = None
|
||||
|
||||
if isinstance(self, self.xmodule_runtime.error_descriptor_class):
|
||||
log.exception('Error creating an ErrorBlock from an ErrorBlock')
|
||||
raise
|
||||
|
||||
log.exception('Error creating xmodule')
|
||||
descriptor = self.xmodule_runtime.error_descriptor_class.from_descriptor(
|
||||
self,
|
||||
error_msg=exc_info_to_str(sys.exc_info())
|
||||
)
|
||||
descriptor.xmodule_runtime = self.xmodule_runtime
|
||||
self.xmodule_runtime.xmodule_instance = descriptor
|
||||
return self.xmodule_runtime.xmodule_instance
|
||||
|
||||
course_id = module_attr('course_id')
|
||||
displayable_items = module_attr('displayable_items')
|
||||
get_display_items = module_attr('get_display_items')
|
||||
get_icon_class = module_attr('get_icon_class')
|
||||
get_progress = module_attr('get_progress')
|
||||
get_score = module_attr('get_score')
|
||||
handle_ajax = module_attr('handle_ajax')
|
||||
student_view = module_attr(STUDENT_VIEW)
|
||||
public_view = module_attr(PUBLIC_VIEW)
|
||||
get_child_descriptors = module_attr('get_child_descriptors')
|
||||
xmodule_handler = module_attr('xmodule_handler')
|
||||
|
||||
# ~~~~~~~~~~~~~~~ XBlock API Wrappers ~~~~~~~~~~~~~~~~
|
||||
def studio_view(self, _context):
|
||||
"""
|
||||
Return a fragment with the html from this XModuleDescriptor's editing view
|
||||
|
||||
Doesn't yet add any of the javascript to the fragment, nor the css.
|
||||
Also doesn't expect any javascript binding, yet.
|
||||
|
||||
Makes no use of the context parameter
|
||||
"""
|
||||
return Fragment(self.get_html())
|
||||
|
||||
|
||||
class ConfigurableFragmentWrapper:
|
||||
"""
|
||||
Runtime mixin that allows for composition of many `wrap_xblock` wrappers
|
||||
@@ -2071,18 +1754,6 @@ class CombinedSystem:
|
||||
self._module_system = module_system
|
||||
self._descriptor_system = descriptor_system
|
||||
|
||||
def _get_student_block(self, block):
|
||||
"""
|
||||
If block is an XModuleDescriptor that has been bound to a student, return
|
||||
the corresponding XModule, instead of the XModuleDescriptor.
|
||||
|
||||
Otherwise, return block.
|
||||
"""
|
||||
if isinstance(block, XModuleDescriptor) and block.xmodule_runtime:
|
||||
return block._xmodule # pylint: disable=protected-access
|
||||
else:
|
||||
return block
|
||||
|
||||
def render(self, block, view_name, context=None):
|
||||
"""
|
||||
Render a block by invoking its view.
|
||||
@@ -2097,9 +1768,6 @@ class CombinedSystem:
|
||||
|
||||
"""
|
||||
context = context or {}
|
||||
if view_name in PREVIEW_VIEWS:
|
||||
block = self._get_student_block(block)
|
||||
|
||||
return self.__getattr__('render')(block, view_name, context)
|
||||
|
||||
def service(self, block, service_name):
|
||||
|
||||
@@ -8,12 +8,13 @@ import os
|
||||
|
||||
from lxml import etree
|
||||
from lxml.etree import Element, ElementTree, XMLParser
|
||||
from xblock.core import XML_NAMESPACES
|
||||
from xblock.core import XBlock, XML_NAMESPACES
|
||||
from xblock.fields import Dict, Scope, ScopeIds
|
||||
from xblock.runtime import KvsFieldData
|
||||
from xmodule.modulestore import EdxJSONEncoder
|
||||
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, own_metadata
|
||||
from xmodule.x_module import XModuleDescriptor # lint-amnesty, pylint: disable=unused-import
|
||||
|
||||
from .x_module import XModuleMixin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -604,5 +605,6 @@ class XmlMixin(XmlParserMixin): # lint-amnesty, pylint: disable=abstract-method
|
||||
super().add_xml_to_node(node)
|
||||
|
||||
|
||||
class XmlDescriptor(XmlMixin, XModuleDescriptor): # lint-amnesty, pylint: disable=abstract-method
|
||||
@XBlock.needs("i18n")
|
||||
class XmlDescriptor(XmlMixin, XModuleMixin): # lint-amnesty, pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user