Merge pull request #24838 from open-craft/symbolist/convert-conditional-module-to-xblock
[BD-04] Convert Conditional XModule to XBlock
This commit is contained in:
@@ -110,10 +110,10 @@ class GetPreviewHtmlTestCase(ModuleStoreTestCase):
|
||||
self.assertNotRegex(html, r"data-block-type=[\"\']test_aside[\"\']")
|
||||
self.assertNotRegex(html, "Aside rendered")
|
||||
|
||||
@mock.patch('xmodule.conditional_module.ConditionalModule.is_condition_satisfied')
|
||||
@mock.patch('xmodule.conditional_module.ConditionalBlock.is_condition_satisfied')
|
||||
def test_preview_conditional_module_children_context(self, mock_is_condition_satisfied):
|
||||
"""
|
||||
Testst that when empty context is pass to children of ConditionalModule it will not raise KeyError.
|
||||
Tests that when empty context is pass to children of ConditionalBlock it will not raise KeyError.
|
||||
"""
|
||||
mock_is_condition_satisfied.return_value = True
|
||||
client = Client()
|
||||
|
||||
@@ -5,7 +5,6 @@ from setuptools import find_packages, setup
|
||||
XMODULES = [
|
||||
"book = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"chapter = xmodule.seq_module:SectionDescriptor",
|
||||
"conditional = xmodule.conditional_module:ConditionalDescriptor",
|
||||
"course = xmodule.course_module:CourseDescriptor",
|
||||
"customtag = xmodule.template_module:CustomTagDescriptor",
|
||||
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
@@ -29,6 +28,7 @@ XMODULES = [
|
||||
]
|
||||
XBLOCKS = [
|
||||
"about = xmodule.html_module:AboutBlock",
|
||||
"conditional = xmodule.conditional_module:ConditionalBlock",
|
||||
"course_info = xmodule.html_module:CourseInfoBlock",
|
||||
"html = xmodule.html_module:HtmlBlock",
|
||||
"library = xmodule.library_root_xblock:LibraryRoot",
|
||||
|
||||
@@ -934,6 +934,8 @@ class CapaMixin(ScorableXBlockMixin, CapaFields):
|
||||
"""
|
||||
True iff full points
|
||||
"""
|
||||
# self.score is initialized in self.lcp but in this method is accessed before self.lcp so just call it first.
|
||||
self.lcp # pylint: disable=pointless-statement
|
||||
return self.score.raw_earned == self.score.raw_possible
|
||||
|
||||
def answer_available(self):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Conditional module is the xmodule, which you can use for disabling
|
||||
some xmodules by conditions.
|
||||
"""
|
||||
ConditionalBlock is an XBlock which you can use for disabling some XBlocks by conditions.
|
||||
"""
|
||||
|
||||
|
||||
@@ -16,11 +16,23 @@ from web_fragments.fragment import Fragment
|
||||
from xblock.fields import ReferenceList, Scope, String
|
||||
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from xmodule.mako_module import MakoTemplateBlockBase
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.studio_editable import StudioEditableDescriptor, StudioEditableModule
|
||||
from xmodule.seq_module import SequenceMixin
|
||||
from xmodule.studio_editable import StudioEditableBlock
|
||||
from xmodule.util.xmodule_django import add_webpack_to_fragment
|
||||
from xmodule.validation import StudioValidation, StudioValidationMessage
|
||||
from xmodule.x_module import STUDENT_VIEW, XModule
|
||||
from xmodule.xml_module import XmlMixin
|
||||
from xmodule.x_module import (
|
||||
HTMLSnippet,
|
||||
ResourceTemplates,
|
||||
shim_xmodule_js,
|
||||
STUDENT_VIEW,
|
||||
XModuleDescriptorToXBlockMixin,
|
||||
XModuleMixin,
|
||||
XModuleToXBlockMixin,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger('edx.' + __name__)
|
||||
|
||||
@@ -28,8 +40,55 @@ log = logging.getLogger('edx.' + __name__)
|
||||
_ = lambda text: text
|
||||
|
||||
|
||||
class ConditionalFields(object):
|
||||
has_children = True
|
||||
class ConditionalBlock(
|
||||
SequenceMixin,
|
||||
MakoTemplateBlockBase,
|
||||
XmlMixin,
|
||||
XModuleDescriptorToXBlockMixin,
|
||||
XModuleToXBlockMixin,
|
||||
HTMLSnippet,
|
||||
ResourceTemplates,
|
||||
XModuleMixin,
|
||||
StudioEditableBlock,
|
||||
):
|
||||
"""
|
||||
Blocks child blocks from showing unless certain conditions are met.
|
||||
|
||||
Example:
|
||||
|
||||
<conditional sources="i4x://.../problem_1; i4x://.../problem_2" completed="True">
|
||||
<show sources="i4x://.../test_6; i4x://.../Avi_resources"/>
|
||||
<video url_name="secret_video" />
|
||||
</conditional>
|
||||
|
||||
<conditional> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
|
||||
submitted - map to `is_submitted` module method.
|
||||
(pressing RESET button makes this function to return False.)
|
||||
|
||||
attempted - map to `is_attempted` module method
|
||||
correct - map to `is_correct` module method
|
||||
poll_answer - map to `poll_answer` module attribute
|
||||
voted - map to `voted` module attribute
|
||||
|
||||
<show> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
|
||||
You can add you own rules for <conditional> tag, like
|
||||
"completed", "attempted" etc. To do that yo must extend
|
||||
`ConditionalBlock.conditions_map` variable and add pair:
|
||||
my_attr: my_property/my_method
|
||||
|
||||
After that you can use it:
|
||||
<conditional my_attr="some value" ...>
|
||||
...
|
||||
</conditional>
|
||||
|
||||
And my_property/my_method will be called for required modules.
|
||||
|
||||
"""
|
||||
|
||||
display_name = String(
|
||||
display_name=_("Display Name"),
|
||||
help=_("The display name for this component."),
|
||||
@@ -57,7 +116,7 @@ class ConditionalFields(object):
|
||||
scope=Scope.content,
|
||||
default='correct',
|
||||
values=lambda: [{'display_name': xml_attr, 'value': xml_attr}
|
||||
for xml_attr in ConditionalModule.conditions_map.keys()]
|
||||
for xml_attr in ConditionalBlock.conditions_map]
|
||||
)
|
||||
|
||||
conditional_value = String(
|
||||
@@ -77,56 +136,39 @@ class ConditionalFields(object):
|
||||
default=_('You must complete {link} before you can access this unit.')
|
||||
)
|
||||
|
||||
has_children = True
|
||||
|
||||
class ConditionalModule(ConditionalFields, XModule, StudioEditableModule):
|
||||
"""
|
||||
Blocks child module from showing unless certain conditions are met.
|
||||
_tag_name = 'conditional'
|
||||
|
||||
Example:
|
||||
resources_dir = None
|
||||
|
||||
<conditional sources="i4x://.../problem_1; i4x://.../problem_2" completed="True">
|
||||
<show sources="i4x://.../test_6; i4x://.../Avi_resources"/>
|
||||
<video url_name="secret_video" />
|
||||
</conditional>
|
||||
filename_extension = "xml"
|
||||
|
||||
<conditional> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
has_score = False
|
||||
|
||||
submitted - map to `is_submitted` module method.
|
||||
(pressing RESET button makes this function to return False.)
|
||||
show_in_read_only_mode = True
|
||||
|
||||
attempted - map to `is_attempted` module method
|
||||
correct - map to `is_correct` module method
|
||||
poll_answer - map to `poll_answer` module attribute
|
||||
voted - map to `voted` module attribute
|
||||
|
||||
<show> tag attributes:
|
||||
sources - location id of required modules, separated by ';'
|
||||
|
||||
You can add you own rules for <conditional> tag, like
|
||||
"completed", "attempted" etc. To do that yo must extend
|
||||
`ConditionalModule.conditions_map` variable and add pair:
|
||||
my_attr: my_property/my_method
|
||||
|
||||
After that you can use it:
|
||||
<conditional my_attr="some value" ...>
|
||||
...
|
||||
</conditional>
|
||||
|
||||
And my_property/my_method will be called for required modules.
|
||||
|
||||
"""
|
||||
|
||||
js = {
|
||||
preview_view_js = {
|
||||
'js': [
|
||||
resource_string(__name__, 'js/src/conditional/display.js'),
|
||||
resource_string(__name__, 'js/src/javascript_loader.js'),
|
||||
resource_string(__name__, 'js/src/collapsible.js'),
|
||||
]
|
||||
],
|
||||
'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'),
|
||||
}
|
||||
preview_view_css = {
|
||||
'scss': [],
|
||||
}
|
||||
|
||||
js_module_name = "Conditional"
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
mako_template = 'widgets/metadata-edit.html'
|
||||
studio_js_module_name = 'SequenceDescriptor'
|
||||
studio_view_js = {
|
||||
'js': [resource_string(__name__, 'js/src/sequence/edit.js')],
|
||||
'xmodule_js': resource_string(__name__, 'js/src/xmodule.js'),
|
||||
}
|
||||
studio_view_css = {
|
||||
'scss': [],
|
||||
}
|
||||
|
||||
# Map
|
||||
# key: <tag attribute in xml>
|
||||
@@ -148,16 +190,29 @@ class ConditionalModule(ConditionalFields, XModule, StudioEditableModule):
|
||||
'voted': 'voted' # poll_question attr
|
||||
}
|
||||
|
||||
@lazy
|
||||
def required_modules(self):
|
||||
return [self.system.get_module(descriptor) for
|
||||
descriptor in self.descriptor.get_required_module_descriptors()]
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Create an instance of the Conditional XBlock.
|
||||
"""
|
||||
super(ConditionalBlock, self).__init__(*args, **kwargs)
|
||||
# Convert sources xml_attribute to a ReferenceList field type so Location/Locator
|
||||
# substitution can be done.
|
||||
if not self.sources_list:
|
||||
if 'sources' in self.xml_attributes and isinstance(self.xml_attributes['sources'], six.string_types):
|
||||
self.sources_list = [
|
||||
# TODO: it is not clear why we are replacing the run here (which actually is a no-op
|
||||
# for old-style course locators. However, this is the implementation of
|
||||
# CourseLocator.make_usage_key_from_deprecated_string, which was previously
|
||||
# being called in this location.
|
||||
BlockUsageLocator.from_string(item).replace(run=self.location.course_key.run)
|
||||
for item in ConditionalBlock.parse_sources(self.xml_attributes)
|
||||
]
|
||||
|
||||
def is_condition_satisfied(self):
|
||||
attr_name = self.conditions_map[self.conditional_attr]
|
||||
|
||||
if self.conditional_value and self.required_modules:
|
||||
for module in self.required_modules:
|
||||
if self.conditional_value and self.get_required_blocks:
|
||||
for module in self.get_required_blocks:
|
||||
if not hasattr(module, attr_name):
|
||||
# We don't throw an exception here because it is possible for
|
||||
# the descriptor of a required module to have a property but
|
||||
@@ -180,15 +235,22 @@ class ConditionalModule(ConditionalFields, XModule, StudioEditableModule):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_html(self):
|
||||
# Calculate html ids of dependencies
|
||||
self.required_html_ids = [descriptor.location.html_id() for
|
||||
descriptor in self.descriptor.get_required_module_descriptors()]
|
||||
def student_view(self, _context):
|
||||
"""
|
||||
Renders the student view.
|
||||
"""
|
||||
fragment = Fragment()
|
||||
fragment.add_content(self.get_html())
|
||||
add_webpack_to_fragment(fragment, 'ConditionalBlockPreview')
|
||||
shim_xmodule_js(fragment, 'Conditional')
|
||||
return fragment
|
||||
|
||||
def get_html(self):
|
||||
required_html_ids = [descriptor.location.html_id() for descriptor in self.get_required_blocks]
|
||||
return self.system.render_template('conditional_ajax.html', {
|
||||
'element_id': self.location.html_id(),
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'depends': ';'.join(self.required_html_ids)
|
||||
'ajax_url': self.ajax_url,
|
||||
'depends': ';'.join(required_html_ids)
|
||||
})
|
||||
|
||||
def author_view(self, context):
|
||||
@@ -206,6 +268,17 @@ class ConditionalModule(ConditionalFields, XModule, StudioEditableModule):
|
||||
|
||||
return fragment
|
||||
|
||||
def studio_view(self, _context):
|
||||
"""
|
||||
Return the studio view.
|
||||
"""
|
||||
fragment = Fragment(
|
||||
self.system.render_template(self.mako_template, self.get_context())
|
||||
)
|
||||
add_webpack_to_fragment(fragment, 'ConditionalBlockStudio')
|
||||
shim_xmodule_js(fragment, self.studio_js_module_name)
|
||||
return fragment
|
||||
|
||||
def handle_ajax(self, _dispatch, _data):
|
||||
"""This is called by courseware.moduleodule_render, to handle
|
||||
an AJAX call.
|
||||
@@ -227,55 +300,14 @@ class ConditionalModule(ConditionalFields, XModule, StudioEditableModule):
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
class_priority = ['video', 'problem']
|
||||
|
||||
child_classes = [self.system.get_module(child_descriptor).get_icon_class()
|
||||
for child_descriptor in self.descriptor.get_children()]
|
||||
child_classes = [
|
||||
child_descriptor.get_icon_class() for child_descriptor in self.get_children()
|
||||
]
|
||||
for c in class_priority:
|
||||
if c in child_classes:
|
||||
new_class = c
|
||||
return new_class
|
||||
|
||||
def validate(self):
|
||||
"""
|
||||
Message for either error or warning validation message/s.
|
||||
|
||||
Returns message and type. Priority given to error type message.
|
||||
"""
|
||||
return self.descriptor.validate()
|
||||
|
||||
|
||||
class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditableDescriptor):
|
||||
"""Descriptor for conditional xmodule."""
|
||||
_tag_name = 'conditional'
|
||||
|
||||
module_class = ConditionalModule
|
||||
|
||||
resources_dir = None
|
||||
|
||||
filename_extension = "xml"
|
||||
|
||||
has_score = False
|
||||
|
||||
show_in_read_only_mode = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Create an instance of the conditional module.
|
||||
"""
|
||||
super(ConditionalDescriptor, self).__init__(*args, **kwargs)
|
||||
|
||||
# Convert sources xml_attribute to a ReferenceList field type so Location/Locator
|
||||
# substitution can be done.
|
||||
if not self.sources_list:
|
||||
if 'sources' in self.xml_attributes and isinstance(self.xml_attributes['sources'], six.string_types):
|
||||
self.sources_list = [
|
||||
# TODO: it is not clear why we are replacing the run here (which actually is a no-op
|
||||
# for old-style course locators. However, this is the implementation of
|
||||
# CourseLocator.make_usage_key_from_deprecated_string, which was previously
|
||||
# being called in this location.
|
||||
BlockUsageLocator.from_string(item).replace(run=self.location.course_key.run)
|
||||
for item in ConditionalDescriptor.parse_sources(self.xml_attributes)
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def parse_sources(xml_element):
|
||||
""" Parse xml_element 'sources' attr and return a list of location strings. """
|
||||
@@ -283,9 +315,16 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditabl
|
||||
if sources:
|
||||
return [location.strip() for location in sources.split(';')]
|
||||
|
||||
@lazy
|
||||
def get_required_blocks(self):
|
||||
"""
|
||||
Returns a list of bound XBlocks instances upon which XBlock depends.
|
||||
"""
|
||||
return [self.system.get_module(descriptor) for descriptor in self.get_required_module_descriptors()]
|
||||
|
||||
def get_required_module_descriptors(self):
|
||||
"""Returns a list of XModuleDescriptor instances upon
|
||||
which this module depends.
|
||||
"""
|
||||
Returns a list of unbound XBlocks instances upon which this XBlock depends.
|
||||
"""
|
||||
descriptors = []
|
||||
for location in self.sources_list:
|
||||
@@ -304,7 +343,7 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditabl
|
||||
children = []
|
||||
show_tag_list = []
|
||||
definition = {}
|
||||
for conditional_attr in six.iterkeys(ConditionalModule.conditions_map):
|
||||
for conditional_attr in six.iterkeys(cls.conditions_map):
|
||||
conditional_value = xml_object.get(conditional_attr)
|
||||
if conditional_value is not None:
|
||||
definition.update({
|
||||
@@ -313,7 +352,7 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditabl
|
||||
})
|
||||
for child in xml_object:
|
||||
if child.tag == 'show':
|
||||
locations = ConditionalDescriptor.parse_sources(child)
|
||||
locations = cls.parse_sources(child)
|
||||
for location in locations:
|
||||
children.append(location)
|
||||
show_tag_list.append(location)
|
||||
@@ -351,7 +390,7 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditabl
|
||||
return xml_object
|
||||
|
||||
def validate(self):
|
||||
validation = super(ConditionalDescriptor, self).validate()
|
||||
validation = super(ConditionalBlock, self).validate()
|
||||
if not self.sources_list:
|
||||
conditional_validation = StudioValidation(self.location)
|
||||
conditional_validation.add(
|
||||
@@ -368,14 +407,9 @@ class ConditionalDescriptor(ConditionalFields, SequenceDescriptor, StudioEditabl
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(ConditionalDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields = super(ConditionalBlock, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([
|
||||
ConditionalDescriptor.due,
|
||||
ConditionalDescriptor.is_practice_exam,
|
||||
ConditionalDescriptor.is_proctored_enabled,
|
||||
ConditionalDescriptor.is_time_limited,
|
||||
ConditionalDescriptor.default_time_limit_minutes,
|
||||
ConditionalDescriptor.show_tag_list,
|
||||
ConditionalDescriptor.exam_review_rules,
|
||||
ConditionalBlock.due,
|
||||
ConditionalBlock.show_tag_list,
|
||||
])
|
||||
return non_editable_fields
|
||||
|
||||
@@ -12,7 +12,6 @@ from datetime import datetime
|
||||
from functools import reduce
|
||||
|
||||
import six
|
||||
from django.contrib.auth.models import User
|
||||
from lxml import etree
|
||||
from opaque_keys.edx.keys import UsageKey
|
||||
from pkg_resources import resource_string
|
||||
@@ -281,6 +280,13 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
datetime.now(UTC) < date
|
||||
)
|
||||
|
||||
def _get_user(self):
|
||||
"""
|
||||
Return the current runtime Django user.
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
return User.objects.get(id=self.runtime.user_id)
|
||||
|
||||
def gate_sequence_if_it_is_a_timed_exam_and_contains_content_type_gated_problems(self):
|
||||
"""
|
||||
Problem:
|
||||
@@ -316,7 +322,7 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
return
|
||||
|
||||
try:
|
||||
user = User.objects.get(id=self.runtime.user_id)
|
||||
user = self._get_user()
|
||||
course_id = self.runtime.course_id
|
||||
content_type_gating_service = self.runtime.service(self, 'content_type_gating')
|
||||
if not (content_type_gating_service and
|
||||
@@ -806,7 +812,46 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
return new_class
|
||||
|
||||
|
||||
class SequenceDescriptor(SequenceFields, ProctoringFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
class SequenceMixin(SequenceFields):
|
||||
"""
|
||||
A mixin of shared code between the SequenceDescriptor and XBlocks
|
||||
converted from XModules which inherited from SequenceDescriptor.
|
||||
"""
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
children = []
|
||||
for child in xml_object:
|
||||
try:
|
||||
child_block = system.process_xml(etree.tostring(child, encoding='unicode'))
|
||||
children.append(child_block.scope_ids.usage_id)
|
||||
except Exception as e:
|
||||
log.exception("Unable to load child when parsing Sequence. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker(u"ERROR: {0}".format(e))
|
||||
continue
|
||||
return {}, children
|
||||
|
||||
def index_dictionary(self):
|
||||
"""
|
||||
Return dictionary prepared with module content and type for indexing.
|
||||
"""
|
||||
# return key/value fields in a Python dict object
|
||||
# values may be numeric / string or dict
|
||||
# default implementation is an empty dict
|
||||
xblock_body = super(SequenceMixin, self).index_dictionary()
|
||||
html_body = {
|
||||
"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"] = "Sequence"
|
||||
|
||||
return xblock_body
|
||||
|
||||
|
||||
class SequenceDescriptor(SequenceMixin, ProctoringFields, MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
A Sequence's Descriptor object
|
||||
"""
|
||||
@@ -822,20 +867,6 @@ class SequenceDescriptor(SequenceFields, ProctoringFields, MakoModuleDescriptor,
|
||||
}
|
||||
js_module_name = "SequenceDescriptor"
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
children = []
|
||||
for child in xml_object:
|
||||
try:
|
||||
child_block = system.process_xml(etree.tostring(child, encoding='unicode'))
|
||||
children.append(child_block.scope_ids.usage_id)
|
||||
except Exception as e:
|
||||
log.exception("Unable to load child when parsing Sequence. Continuing...")
|
||||
if system.error_tracker is not None:
|
||||
system.error_tracker(u"ERROR: {0}".format(e))
|
||||
continue
|
||||
return {}, children
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('sequential')
|
||||
for child in self.get_children():
|
||||
@@ -848,28 +879,9 @@ class SequenceDescriptor(SequenceFields, ProctoringFields, MakoModuleDescriptor,
|
||||
`is_entrance_exam` should not be editable in the Studio settings editor.
|
||||
"""
|
||||
non_editable_fields = super(SequenceDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.append(self.fields['is_entrance_exam'])
|
||||
non_editable_fields.append(self.fields['is_entrance_exam']) # pylint:disable=unsubscriptable-object
|
||||
return non_editable_fields
|
||||
|
||||
def index_dictionary(self):
|
||||
"""
|
||||
Return dictionary prepared with module content and type for indexing.
|
||||
"""
|
||||
# return key/value fields in a Python dict object
|
||||
# values may be numeric / string or dict
|
||||
# default implementation is an empty dict
|
||||
xblock_body = super(SequenceDescriptor, self).index_dictionary()
|
||||
html_body = {
|
||||
"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"] = "Sequence"
|
||||
|
||||
return xblock_body
|
||||
|
||||
|
||||
class HighlightsFields(object):
|
||||
"""Only Sections have summaries now, but we may expand that later."""
|
||||
|
||||
@@ -21,6 +21,7 @@ from docopt import docopt
|
||||
from path import Path as path
|
||||
|
||||
from xmodule.capa_module import ProblemBlock
|
||||
from xmodule.conditional_module import ConditionalBlock
|
||||
from xmodule.html_module import AboutBlock, CourseInfoBlock, HtmlBlock, StaticTabBlock
|
||||
from xmodule.library_content_module import LibraryContentBlock
|
||||
from xmodule.word_cloud_module import WordCloudBlock
|
||||
@@ -66,6 +67,7 @@ class VideoBlock(HTMLSnippet):
|
||||
# Should only be used for XModules being converted to XBlocks.
|
||||
XBLOCK_CLASSES = [
|
||||
AboutBlock,
|
||||
ConditionalBlock,
|
||||
CourseInfoBlock,
|
||||
HtmlBlock,
|
||||
LibraryContentBlock,
|
||||
|
||||
@@ -13,7 +13,7 @@ from web_fragments.fragment import Fragment
|
||||
from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from xmodule.conditional_module import ConditionalDescriptor
|
||||
from xmodule.conditional_module import ConditionalBlock
|
||||
from xmodule.error_module import NonStaffErrorDescriptor
|
||||
from xmodule.modulestore.xml import CourseLocationManager, ImportSystem, XMLModuleStore
|
||||
from xmodule.tests import DATA_DIR, get_test_descriptor_system, get_test_system
|
||||
@@ -45,9 +45,9 @@ class DummySystem(ImportSystem):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
|
||||
class ConditionalModuleFactory(xml.XmlImportFactory):
|
||||
class ConditionalBlockFactory(xml.XmlImportFactory):
|
||||
"""
|
||||
Factory for generating ConditionalModule for testing purposes
|
||||
Factory for generating ConditionalBlock for testing purposes
|
||||
"""
|
||||
tag = 'conditional'
|
||||
|
||||
@@ -125,17 +125,15 @@ class ConditionalFactory(object):
|
||||
'children': [child_descriptor.location],
|
||||
})
|
||||
|
||||
cond_descriptor = ConditionalDescriptor(
|
||||
cond_descriptor = ConditionalBlock(
|
||||
descriptor_system,
|
||||
field_data,
|
||||
ScopeIds(None, None, cond_location, cond_location)
|
||||
)
|
||||
cond_descriptor.xmodule_runtime = system
|
||||
system.get_module = lambda desc: desc if visible_to_nonstaff_users(desc) else None
|
||||
cond_descriptor.get_required_module_descriptors = Mock(return_value=[source_descriptor])
|
||||
cond_descriptor.required_modules = [
|
||||
system.get_module(descriptor)
|
||||
for descriptor in cond_descriptor.get_required_module_descriptors()
|
||||
cond_descriptor.get_required_blocks = [
|
||||
system.get_module(source_descriptor),
|
||||
]
|
||||
|
||||
# return dict:
|
||||
@@ -144,14 +142,14 @@ class ConditionalFactory(object):
|
||||
'child_module': child_descriptor}
|
||||
|
||||
|
||||
class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
class ConditionalBlockBasicTest(unittest.TestCase):
|
||||
"""
|
||||
Make sure that conditional module works, using mocks for
|
||||
other modules.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(ConditionalModuleBasicTest, self).setUp()
|
||||
super(ConditionalBlockBasicTest, self).setUp()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
def test_icon_class(self):
|
||||
@@ -169,7 +167,7 @@ class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
# we reverse it here
|
||||
html = modules['cond_module'].render(STUDENT_VIEW).content
|
||||
expected = modules['cond_module'].xmodule_runtime.render_template('conditional_ajax.html', {
|
||||
'ajax_url': modules['cond_module'].xmodule_runtime.ajax_url,
|
||||
'ajax_url': modules['cond_module'].ajax_url,
|
||||
'element_id': u'i4x-edX-conditional_test-conditional-SampleConditional',
|
||||
'depends': u'i4x-edX-conditional_test-problem-SampleProblem',
|
||||
})
|
||||
@@ -214,12 +212,12 @@ class ConditionalModuleBasicTest(unittest.TestCase):
|
||||
cond_module.is_attempted = "false"
|
||||
cond_module.handle_ajax('', '')
|
||||
self.assertFalse(mock_log.warn.called)
|
||||
self.assertIn(None, cond_module.required_modules)
|
||||
self.assertIn(None, cond_module.get_required_blocks)
|
||||
|
||||
|
||||
class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
class ConditionalBlockXmlTest(unittest.TestCase):
|
||||
"""
|
||||
Make sure ConditionalModule works, by loading data in from an XML-defined course.
|
||||
Make sure ConditionalBlock works, by loading data in from an XML-defined course.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@@ -228,7 +226,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
return DummySystem(load_error_modules)
|
||||
|
||||
def setUp(self):
|
||||
super(ConditionalModuleXmlTest, self).setUp()
|
||||
super(ConditionalBlockXmlTest, self).setUp()
|
||||
self.test_system = get_test_system()
|
||||
|
||||
def get_course(self, name):
|
||||
@@ -241,7 +239,8 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
self.assertEqual(len(courses), 1)
|
||||
return courses[0]
|
||||
|
||||
def test_conditional_module(self):
|
||||
@patch('xmodule.x_module.descriptor_global_local_resource_url')
|
||||
def test_conditional_module(self, _):
|
||||
"""Make sure that conditional module works"""
|
||||
|
||||
print("Starting import")
|
||||
@@ -309,9 +308,11 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
fragments = ajax['fragments']
|
||||
self.assertTrue(any(['This is a secret' in item['content'] for item in fragments]))
|
||||
|
||||
maxDiff = None
|
||||
|
||||
def test_conditional_module_with_empty_sources_list(self):
|
||||
"""
|
||||
If a ConditionalDescriptor is initialized with an empty sources_list, we assert that the sources_list is set
|
||||
If a ConditionalBlock is initialized with an empty sources_list, we assert that the sources_list is set
|
||||
via generating UsageKeys from the values in xml_attributes['sources']
|
||||
"""
|
||||
dummy_system = Mock()
|
||||
@@ -323,16 +324,18 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
'xml_attributes': {'sources': 'i4x://HarvardX/ER22x/poll_question/T15_poll'},
|
||||
'children': None,
|
||||
})
|
||||
conditional = ConditionalDescriptor(
|
||||
conditional = ConditionalBlock(
|
||||
dummy_system,
|
||||
dummy_field_data,
|
||||
dummy_scope_ids,
|
||||
)
|
||||
|
||||
new_run = conditional.location.course_key.run
|
||||
self.assertEqual(
|
||||
conditional.sources_list[0],
|
||||
# Matching what is in ConditionalDescriptor.__init__.
|
||||
BlockUsageLocator.from_string(conditional.xml_attributes['sources']).replace(run=new_run)
|
||||
BlockUsageLocator.from_string(
|
||||
conditional.xml_attributes['sources']
|
||||
).replace(run=dummy_location.course_key.run)
|
||||
)
|
||||
|
||||
def test_conditional_module_parse_sources(self):
|
||||
@@ -345,7 +348,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
'xml_attributes': {'sources': 'i4x://HarvardX/ER22x/poll_question/T15_poll;i4x://HarvardX/ER22x/poll_question/T16_poll'},
|
||||
'children': None,
|
||||
})
|
||||
conditional = ConditionalDescriptor(
|
||||
conditional = ConditionalBlock(
|
||||
dummy_system,
|
||||
dummy_field_data,
|
||||
dummy_scope_ids,
|
||||
@@ -358,7 +361,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
def test_conditional_module_parse_attr_values(self):
|
||||
root = '<conditional attempted="false"></conditional>'
|
||||
xml_object = etree.XML(root)
|
||||
definition = ConditionalDescriptor.definition_from_xml(xml_object, Mock())[0]
|
||||
definition = ConditionalBlock.definition_from_xml(xml_object, Mock())[0]
|
||||
expected_definition = {
|
||||
'show_tag_list': [],
|
||||
'conditional_attr': 'attempted',
|
||||
@@ -380,16 +383,16 @@ class ConditionalModuleXmlTest(unittest.TestCase):
|
||||
self.assertDictEqual(modules['cond_module'].xml_attributes, expected_xml_attributes)
|
||||
|
||||
|
||||
class ConditionalModuleStudioTest(XModuleXmlImportTest):
|
||||
class ConditionalBlockStudioTest(XModuleXmlImportTest):
|
||||
"""
|
||||
Unit tests for how conditional test interacts with Studio.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(ConditionalModuleStudioTest, self).setUp()
|
||||
super().setUp()
|
||||
course = xml.CourseFactory.build()
|
||||
sequence = xml.SequenceFactory.build(parent=course)
|
||||
conditional = ConditionalModuleFactory(
|
||||
conditional = ConditionalBlockFactory(
|
||||
parent=sequence,
|
||||
attribs={
|
||||
'group_id_to_child': '{"0": "i4x://edX/xml_test_course/html/conditional_0"}'
|
||||
@@ -438,11 +441,7 @@ class ConditionalModuleStudioTest(XModuleXmlImportTest):
|
||||
Test the settings that are marked as "non-editable".
|
||||
"""
|
||||
non_editable_metadata_fields = self.conditional.non_editable_metadata_fields
|
||||
self.assertIn(ConditionalDescriptor.due, non_editable_metadata_fields)
|
||||
self.assertIn(ConditionalDescriptor.is_practice_exam, non_editable_metadata_fields)
|
||||
self.assertIn(ConditionalDescriptor.is_time_limited, non_editable_metadata_fields)
|
||||
self.assertIn(ConditionalDescriptor.default_time_limit_minutes, non_editable_metadata_fields)
|
||||
self.assertIn(ConditionalDescriptor.show_tag_list, non_editable_metadata_fields)
|
||||
self.assertIn(ConditionalBlock.due, non_editable_metadata_fields)
|
||||
|
||||
def test_validation_messages(self):
|
||||
"""
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test for Conditional Xmodule functional logic."""
|
||||
|
||||
|
||||
from xmodule.conditional_module import ConditionalDescriptor
|
||||
|
||||
from . import LogicTest
|
||||
|
||||
|
||||
class ConditionalModuleTest(LogicTest):
|
||||
"""Logic tests for Conditional Xmodule."""
|
||||
descriptor_class = ConditionalDescriptor
|
||||
|
||||
def test_ajax_request(self):
|
||||
"Make sure that ajax request works correctly"
|
||||
# Mock is_condition_satisfied
|
||||
self.xmodule.is_condition_satisfied = lambda: True
|
||||
self.xmodule.descriptor.get_children = lambda: []
|
||||
|
||||
response = self.ajax_request('No', {})
|
||||
fragments = response['fragments']
|
||||
|
||||
self.assertEqual(fragments, [])
|
||||
@@ -160,7 +160,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
self.assertIn("'prev_url': 'PrevSequential'", html)
|
||||
self.assertNotIn("fa fa-check-circle check-circle is-hidden", html)
|
||||
|
||||
@patch('xmodule.seq_module.User.objects.get', return_value=UserFactory.build())
|
||||
@patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build())
|
||||
def test_timed_exam_gating_waffle_flag(self, mocked_user):
|
||||
"""
|
||||
Verify the code inside the waffle flag is not executed with the flag off
|
||||
@@ -185,7 +185,7 @@ class SequenceBlockTestCase(XModuleXmlImportTest):
|
||||
mocked_user.assert_called_once()
|
||||
|
||||
@override_waffle_flag(TIMED_EXAM_GATING_WAFFLE_FLAG, active=True)
|
||||
@patch('xmodule.seq_module.User.objects.get', return_value=UserFactory.build())
|
||||
@patch('xmodule.seq_module.SequenceModule._get_user', return_value=UserFactory.build())
|
||||
def test_that_timed_sequence_gating_respects_access_configurations(self, mocked_user): # pylint: disable=unused-argument
|
||||
"""
|
||||
Verify that if a time limited sequence contains content type gated problems, we gate the sequence
|
||||
|
||||
@@ -31,7 +31,7 @@ from xblock.field_data import DictFieldData
|
||||
from xblock.fields import ScopeIds
|
||||
|
||||
from xmodule.annotatable_module import AnnotatableDescriptor
|
||||
from xmodule.conditional_module import ConditionalDescriptor
|
||||
from xmodule.conditional_module import ConditionalBlock
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.html_module import HtmlBlock
|
||||
from xmodule.poll_module import PollDescriptor
|
||||
@@ -66,7 +66,7 @@ LEAF_XMODULES = {
|
||||
# to a list of sample field values to test with.
|
||||
# TODO: Add more types of sample data
|
||||
CONTAINER_XMODULES = {
|
||||
ConditionalDescriptor: [{}],
|
||||
ConditionalBlock: [{}],
|
||||
CourseDescriptor: [{}],
|
||||
RandomizeDescriptor: [{'display_name': 'Test String Display'}],
|
||||
SequenceDescriptor: [{'display_name': u'Test Unicode हिंदी Display'}],
|
||||
|
||||
@@ -14,7 +14,7 @@ def _message(reqm, message):
|
||||
url_name = reqm.display_name_with_default))
|
||||
%>
|
||||
% if message:
|
||||
% for reqm in module.required_modules:
|
||||
% for reqm in module.get_required_blocks:
|
||||
% if reqm:
|
||||
<p class="conditional-message">${_message(reqm, message)}</p>
|
||||
% else:
|
||||
|
||||
Reference in New Issue
Block a user