Merge pull request #24809 from open-craft/symbolist/convert-randomize-module-to-xblock
[BD-04] Convert Randomize XModule to XBlock
This commit is contained in:
@@ -13,7 +13,6 @@ XMODULES = [
|
||||
"nonstaff_error = xmodule.error_module:NonStaffErrorDescriptor",
|
||||
"poll_question = xmodule.poll_module:PollDescriptor",
|
||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||
"randomize = xmodule.randomize_module:RandomizeDescriptor",
|
||||
"split_test = xmodule.split_test_module:SplitTestDescriptor",
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
@@ -35,6 +34,7 @@ XBLOCKS = [
|
||||
"library_content = xmodule.library_content_module:LibraryContentBlock",
|
||||
"library_sourced = xmodule.library_sourced_block:LibrarySourcedBlock",
|
||||
"problem = xmodule.capa_module:ProblemBlock",
|
||||
"randomize = xmodule.randomize_module:RandomizeBlock",
|
||||
"static_tab = xmodule.html_module:StaticTabBlock",
|
||||
"unit = xmodule.unit_block:UnitBlock",
|
||||
"vertical = xmodule.vertical_block:VerticalBlock",
|
||||
|
||||
@@ -3,22 +3,37 @@
|
||||
import logging
|
||||
import random
|
||||
|
||||
from django.utils.functional import cached_property
|
||||
from lxml import etree
|
||||
from web_fragments.fragment import Fragment
|
||||
from xblock.fields import Integer, Scope
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.x_module import STUDENT_VIEW, XModule
|
||||
from xmodule.mako_module import MakoTemplateBlockBase
|
||||
from xmodule.seq_module import SequenceMixin
|
||||
from xmodule.xml_module import XmlMixin
|
||||
from xmodule.x_module import (
|
||||
HTMLSnippet,
|
||||
ResourceTemplates,
|
||||
STUDENT_VIEW,
|
||||
XModuleDescriptorToXBlockMixin,
|
||||
XModuleMixin,
|
||||
XModuleToXBlockMixin,
|
||||
)
|
||||
|
||||
log = logging.getLogger('edx.' + __name__)
|
||||
|
||||
|
||||
class RandomizeFields(object):
|
||||
choice = Integer(help="Which random child was chosen", scope=Scope.user_state)
|
||||
|
||||
|
||||
class RandomizeModule(RandomizeFields, XModule):
|
||||
class RandomizeBlock(
|
||||
SequenceMixin,
|
||||
MakoTemplateBlockBase,
|
||||
XmlMixin,
|
||||
XModuleDescriptorToXBlockMixin,
|
||||
XModuleToXBlockMixin,
|
||||
HTMLSnippet,
|
||||
ResourceTemplates,
|
||||
XModuleMixin,
|
||||
):
|
||||
"""
|
||||
Chooses a random child module. Chooses the same one every time for each student.
|
||||
Chooses a random child xblock. Chooses the same one every time for each student.
|
||||
|
||||
Example:
|
||||
<randomize>
|
||||
@@ -38,11 +53,18 @@ class RandomizeModule(RandomizeFields, XModule):
|
||||
grading interaction is a tangle between super and subclasses of descriptors and
|
||||
modules.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RandomizeModule, self).__init__(*args, **kwargs)
|
||||
choice = Integer(help="Which random child was chosen", scope=Scope.user_state)
|
||||
|
||||
# NOTE: calling self.get_children() doesn't work until we've picked a choice
|
||||
num_choices = len(self.descriptor.get_children())
|
||||
resources_dir = None
|
||||
|
||||
filename_extension = "xml"
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
@cached_property
|
||||
def child(self):
|
||||
""" Return XBlock instance of selected choice """
|
||||
num_choices = len(self.get_children())
|
||||
|
||||
if self.choice is not None and self.choice > num_choices:
|
||||
# Oops. Children changed. Reset.
|
||||
@@ -56,54 +78,40 @@ class RandomizeModule(RandomizeFields, XModule):
|
||||
else:
|
||||
self.choice = random.randrange(0, num_choices)
|
||||
|
||||
if self.choice is not None:
|
||||
# Now get_children() should return a list with one element
|
||||
log.debug("children of randomize module (should be only 1): %s", self.child)
|
||||
|
||||
@property
|
||||
def child_descriptor(self):
|
||||
""" Return descriptor of selected choice """
|
||||
if self.choice is None:
|
||||
return None
|
||||
return self.descriptor.get_children()[self.choice]
|
||||
child = self.get_children()[self.choice]
|
||||
|
||||
@property
|
||||
def child(self):
|
||||
""" Return module instance of selected choice """
|
||||
child_descriptor = self.child_descriptor
|
||||
if child_descriptor is None:
|
||||
return None
|
||||
return self.system.get_module(child_descriptor)
|
||||
if self.choice is not None:
|
||||
log.debug("children of randomize module (should be only 1): %s", child)
|
||||
|
||||
return child
|
||||
|
||||
def get_child_descriptors(self):
|
||||
"""
|
||||
For grading--return just the chosen child.
|
||||
"""
|
||||
if self.child_descriptor is None:
|
||||
if self.child is None:
|
||||
return []
|
||||
|
||||
return [self.child_descriptor]
|
||||
return [self.child]
|
||||
|
||||
def student_view(self, context):
|
||||
"""
|
||||
The student view.
|
||||
"""
|
||||
if self.child is None:
|
||||
# raise error instead? In fact, could complain on descriptor load...
|
||||
return Fragment(content=u"<div>Nothing to randomize between</div>")
|
||||
|
||||
return self.child.render(STUDENT_VIEW, context)
|
||||
|
||||
def get_html(self):
|
||||
return self.studio_view(None).content
|
||||
|
||||
def get_icon_class(self):
|
||||
return self.child.get_icon_class() if self.child else 'other'
|
||||
|
||||
|
||||
class RandomizeDescriptor(RandomizeFields, SequenceDescriptor):
|
||||
# the editing interface can be the same as for sequences -- just a container
|
||||
module_class = RandomizeModule
|
||||
resources_dir = None
|
||||
|
||||
filename_extension = "xml"
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
|
||||
xml_object = etree.Element('randomize')
|
||||
|
||||
@@ -88,7 +88,10 @@ class TestModuleSystem(ModuleSystem): # pylint: disable=abstract-method
|
||||
return rt_repr
|
||||
|
||||
|
||||
def get_test_system(course_id=CourseKey.from_string('/'.join(['org', 'course', 'run']))):
|
||||
def get_test_system(
|
||||
course_id=CourseKey.from_string('/'.join(['org', 'course', 'run'])),
|
||||
user=None,
|
||||
):
|
||||
"""
|
||||
Construct a test ModuleSystem instance.
|
||||
|
||||
@@ -101,7 +104,8 @@ def get_test_system(course_id=CourseKey.from_string('/'.join(['org', 'course', '
|
||||
where `my_render_func` is a function of the form my_render_func(template, context).
|
||||
|
||||
"""
|
||||
user = Mock(name='get_test_system.user', is_staff=False)
|
||||
if not user:
|
||||
user = Mock(name='get_test_system.user', is_staff=False)
|
||||
|
||||
descriptor_system = get_test_descriptor_system()
|
||||
|
||||
|
||||
@@ -2,108 +2,117 @@
|
||||
Test cases covering workflows and behaviors for the Randomize XModule
|
||||
"""
|
||||
|
||||
from fs.memoryfs import MemoryFS
|
||||
from lxml import etree
|
||||
from mock import Mock
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.utils import MixedSplitTestCase
|
||||
from xmodule.randomize_module import RandomizeBlock
|
||||
from xmodule.tests import get_test_system
|
||||
|
||||
from opaque_keys.edx.locator import BlockUsageLocator
|
||||
from pytz import UTC
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from xmodule.randomize_module import RandomizeModule
|
||||
|
||||
from .test_course_module import DummySystem as DummyImportSystem
|
||||
|
||||
ORG = 'test_org'
|
||||
COURSE = 'test_course'
|
||||
|
||||
START = '2013-01-01T01:00:00'
|
||||
_TODAY = datetime.now(UTC)
|
||||
_LAST_WEEK = _TODAY - timedelta(days=7)
|
||||
_NEXT_WEEK = _TODAY + timedelta(days=7)
|
||||
from .test_course_module import DummySystem as TestImportSystem
|
||||
|
||||
|
||||
class RandomizeModuleTestCase(unittest.TestCase):
|
||||
"""Make sure the randomize module works"""
|
||||
class RandomizeBlockTest(MixedSplitTestCase):
|
||||
"""
|
||||
Base class for tests of LibraryContentModule (library_content_module.py)
|
||||
"""
|
||||
maxDiff = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.course = CourseFactory.create(modulestore=self.store)
|
||||
self.chapter = self.make_block("chapter", self.course)
|
||||
self.sequential = self.make_block("sequential", self.chapter)
|
||||
self.vertical = self.make_block("vertical", self.sequential)
|
||||
self.randomize_block = self.make_block(
|
||||
"randomize",
|
||||
self.vertical,
|
||||
display_name="Hello Randomize",
|
||||
)
|
||||
self.child_blocks = [
|
||||
self.make_block("html", self.randomize_block, display_name="Hello HTML {}".format(i))
|
||||
for i in range(1, 4)
|
||||
]
|
||||
|
||||
def _bind_module_system(self, block, user_id):
|
||||
"""
|
||||
Initialize dummy testing course.
|
||||
Bind module system to block so we can access student-specific data.
|
||||
"""
|
||||
super(RandomizeModuleTestCase, self).setUp()
|
||||
self.system = DummyImportSystem(load_error_modules=True)
|
||||
self.system.seed = None
|
||||
self.course = self.get_dummy_course()
|
||||
self.modulestore = self.system.modulestore
|
||||
user = Mock(name='get_test_system.user', id=user_id, is_staff=False)
|
||||
module_system = get_test_system(course_id=block.location.course_key, user=user)
|
||||
module_system.descriptor_runtime = block.runtime._descriptor_system # pylint: disable=protected-access
|
||||
block.xmodule_runtime = module_system
|
||||
|
||||
def get_dummy_course(self, start=_TODAY):
|
||||
"""Get a dummy course"""
|
||||
|
||||
self.start_xml = '''
|
||||
<course org="{org}" course="{course}"
|
||||
graceperiod="1 day" url_name="test"
|
||||
start="{start}">
|
||||
<chapter url="ch1" url_name="chapter1" display_name="CH1">
|
||||
<randomize url_name="my_randomize">
|
||||
<html url_name="a" display_name="A">Two houses, ...</html>
|
||||
<html url_name="b" display_name="B">Three houses, ...</html>
|
||||
</randomize>
|
||||
</chapter>
|
||||
<chapter url="ch2" url_name="chapter2" display_name="CH2">
|
||||
</chapter>
|
||||
</course>
|
||||
'''.format(org=ORG, course=COURSE, start=start)
|
||||
|
||||
return self.system.process_xml(self.start_xml)
|
||||
|
||||
def test_import(self):
|
||||
def test_xml_export_import_cycle(self):
|
||||
"""
|
||||
Just make sure descriptor loads without error
|
||||
Test the export-import cycle.
|
||||
"""
|
||||
self.get_dummy_course(START)
|
||||
randomize_block = self.store.get_item(self.randomize_block.location)
|
||||
|
||||
def test_course_has_started(self):
|
||||
"""
|
||||
Test CourseDescriptor.has_started.
|
||||
"""
|
||||
self.course.start = _LAST_WEEK
|
||||
self.assertTrue(self.course.has_started())
|
||||
self.course.start = _NEXT_WEEK
|
||||
self.assertFalse(self.course.has_started())
|
||||
|
||||
def test_children(self):
|
||||
""" Check course/randomize module works fine """
|
||||
|
||||
self.assertTrue(self.course.has_children)
|
||||
self.assertEqual(len(self.course.get_children()), 2)
|
||||
|
||||
def inner_get_module(descriptor):
|
||||
"""
|
||||
Override systems.get_module
|
||||
This method will be called when any call is made to self.system.get_module
|
||||
"""
|
||||
if isinstance(descriptor, BlockUsageLocator):
|
||||
location = descriptor
|
||||
descriptor = self.modulestore.get_item(location, depth=None)
|
||||
descriptor.xmodule_runtime = self.get_dummy_course()
|
||||
descriptor.xmodule_runtime.descriptor_runtime = descriptor._runtime # pylint: disable=protected-access
|
||||
descriptor.xmodule_runtime.get_module = inner_get_module
|
||||
return descriptor
|
||||
|
||||
self.system.get_module = inner_get_module
|
||||
|
||||
# Get randomize_descriptor from the course & verify its children
|
||||
randomize_descriptor = inner_get_module(self.course.id.make_usage_key('randomize', 'my_randomize'))
|
||||
self.assertTrue(randomize_descriptor.has_children)
|
||||
self.assertEqual(len(randomize_descriptor.get_children()), 2)
|
||||
|
||||
# Call RandomizeModule which will select an element from the list of available items
|
||||
randomize_module = RandomizeModule(
|
||||
randomize_descriptor,
|
||||
self.system,
|
||||
scope_ids=ScopeIds(None, None, self.course.id, self.course.id)
|
||||
expected_olx = (
|
||||
'<randomize display_name="{block.display_name}">\n'
|
||||
' <html url_name="{block.children[0].block_id}"/>\n'
|
||||
' <html url_name="{block.children[1].block_id}"/>\n'
|
||||
' <html url_name="{block.children[2].block_id}"/>\n'
|
||||
'</randomize>\n'
|
||||
).format(
|
||||
block=randomize_block,
|
||||
)
|
||||
|
||||
# Verify the selected child
|
||||
self.assertEqual(len(randomize_module.get_child_descriptors()), 1, "No child is chosen")
|
||||
self.assertIn(randomize_module.child.display_name, ['A', 'B'], "Unwanted child selected")
|
||||
export_fs = MemoryFS()
|
||||
# Set the virtual FS to export the olx to.
|
||||
randomize_block.runtime._descriptor_system.export_fs = export_fs # pylint: disable=protected-access
|
||||
|
||||
# Export the olx.
|
||||
node = etree.Element("unknown_root")
|
||||
randomize_block.add_xml_to_node(node)
|
||||
|
||||
# Read it back
|
||||
with export_fs.open('{dir}/{file_name}.xml'.format(
|
||||
dir=randomize_block.scope_ids.usage_id.block_type,
|
||||
file_name=randomize_block.scope_ids.usage_id.block_id
|
||||
)) as f:
|
||||
exported_olx = f.read()
|
||||
|
||||
# And compare.
|
||||
self.assertEqual(exported_olx, expected_olx)
|
||||
|
||||
runtime = TestImportSystem(load_error_modules=True, course_id=randomize_block.location.course_key)
|
||||
runtime.resources_fs = export_fs
|
||||
|
||||
# Now import it.
|
||||
olx_element = etree.fromstring(exported_olx)
|
||||
id_generator = Mock()
|
||||
imported_randomize_block = RandomizeBlock.parse_xml(olx_element, runtime, None, id_generator)
|
||||
|
||||
# Check the new XBlock has the same properties as the old one.
|
||||
self.assertEqual(imported_randomize_block.display_name, randomize_block.display_name)
|
||||
self.assertEqual(len(imported_randomize_block.children), 3)
|
||||
self.assertEqual(imported_randomize_block.children, randomize_block.children)
|
||||
|
||||
def test_children_seen_by_a_user(self):
|
||||
"""
|
||||
Test that each student sees only one block as a child of the LibraryContent block.
|
||||
"""
|
||||
randomize_block = self.store.get_item(self.randomize_block.location)
|
||||
self._bind_module_system(randomize_block, 3)
|
||||
|
||||
# Make sure the runtime knows that the block's children vary per-user:
|
||||
self.assertTrue(randomize_block.has_dynamic_children())
|
||||
|
||||
self.assertEqual(len(randomize_block.children), 3)
|
||||
|
||||
# Check how many children each user will see:
|
||||
self.assertEqual(len(randomize_block.get_child_descriptors()), 1)
|
||||
self.assertEqual(randomize_block.get_child_descriptors()[0].display_name, 'Hello HTML 1')
|
||||
# Check that get_content_titles() doesn't return titles for hidden/unused children
|
||||
# get_content_titles() is not overridden in RandomizeBlock so titles of the 3 children are returned.
|
||||
self.assertEqual(len(randomize_block.get_content_titles()), 3)
|
||||
|
||||
# Bind to another user and check a different child block is displayed to user.
|
||||
randomize_block = self.store.get_item(self.randomize_block.location)
|
||||
self._bind_module_system(randomize_block, 1)
|
||||
self.assertEqual(randomize_block.get_child_descriptors()[0].display_name, 'Hello HTML 2')
|
||||
|
||||
@@ -35,7 +35,7 @@ from xmodule.conditional_module import ConditionalBlock
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.html_module import HtmlBlock
|
||||
from xmodule.poll_module import PollDescriptor
|
||||
from xmodule.randomize_module import RandomizeDescriptor
|
||||
from xmodule.randomize_module import RandomizeBlock
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.tests import get_test_descriptor_system, get_test_system
|
||||
from xmodule.vertical_block import VerticalBlock
|
||||
@@ -68,7 +68,7 @@ LEAF_XMODULES = {
|
||||
CONTAINER_XMODULES = {
|
||||
ConditionalBlock: [{}],
|
||||
CourseDescriptor: [{}],
|
||||
RandomizeDescriptor: [{'display_name': 'Test String Display'}],
|
||||
RandomizeBlock: [{'display_name': 'Test String Display'}],
|
||||
SequenceDescriptor: [{'display_name': u'Test Unicode हिंदी Display'}],
|
||||
VerticalBlock: [{}],
|
||||
WrapperBlock: [{}],
|
||||
|
||||
Reference in New Issue
Block a user