670 lines
26 KiB
Python
670 lines
26 KiB
Python
# disable missing docstring
|
|
# pylint: disable=missing-docstring
|
|
|
|
|
|
import unittest
|
|
from unittest.mock import Mock
|
|
import dateutil.parser
|
|
|
|
from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
|
|
from xblock.field_data import DictFieldData
|
|
from xblock.fields import Any, Boolean, Dict, Float, Integer, List, Scope, String
|
|
from xblock.runtime import DictKeyValueStore, KvsFieldData
|
|
|
|
from xmodule.course_block import CourseBlock
|
|
from xmodule.fields import Date, RelativeTime, Timedelta
|
|
from xmodule.modulestore.inheritance import InheritanceKeyValueStore, InheritanceMixin, InheritingFieldData
|
|
from xmodule.modulestore.split_mongo.split_mongo_kvs import SplitMongoKVS
|
|
from xmodule.seq_block import SequenceBlock
|
|
from xmodule.tests import get_test_descriptor_system
|
|
from xmodule.tests.xml import XModuleXmlImportTest
|
|
from xmodule.tests.xml.factories import CourseFactory, ProblemFactory, SequenceFactory
|
|
from xmodule.x_module import XModuleMixin
|
|
from xmodule.xml_module import XmlMixin, deserialize_field, serialize_field
|
|
|
|
|
|
class CrazyJsonString(String):
|
|
def to_json(self, value):
|
|
return value + " JSON"
|
|
|
|
|
|
class TestFields:
|
|
# Will be returned by editable_metadata_fields.
|
|
max_attempts = Integer(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
|
|
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
|
|
due = Date(scope=Scope.settings)
|
|
# Will not be returned by editable_metadata_fields because is not Scope.settings.
|
|
student_answers = Dict(scope=Scope.user_state)
|
|
# Will be returned, and can override the inherited value from XModule.
|
|
display_name = String(
|
|
scope=Scope.settings,
|
|
default='local default',
|
|
display_name='Local Display Name',
|
|
help='local help'
|
|
)
|
|
# Used for testing select type, effect of to_json method
|
|
string_select = CrazyJsonString(
|
|
scope=Scope.settings,
|
|
default='default value',
|
|
values=[{'display_name': 'first', 'value': 'value a'},
|
|
{'display_name': 'second', 'value': 'value b'}]
|
|
)
|
|
showanswer = InheritanceMixin.showanswer
|
|
# Used for testing select type
|
|
float_select = Float(scope=Scope.settings, default=.999, values=[1.23, 0.98])
|
|
# Used for testing float type
|
|
float_non_select = Float(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
|
|
# Used for testing that Booleans get mapped to select type
|
|
boolean_select = Boolean(scope=Scope.settings)
|
|
# Used for testing Lists
|
|
list_field = List(scope=Scope.settings, default=[])
|
|
|
|
|
|
class InheritingFieldDataTest(unittest.TestCase):
|
|
"""
|
|
Tests of InheritingFieldData.
|
|
"""
|
|
|
|
class TestableInheritingXBlock(XmlMixin): # lint-amnesty, pylint: disable=abstract-method
|
|
"""
|
|
An XBlock we can use in these tests.
|
|
"""
|
|
inherited = String(scope=Scope.settings, default="the default")
|
|
not_inherited = String(scope=Scope.settings, default="nothing")
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.dummy_course_key = CourseLocator('test_org', 'test_123', 'test_run')
|
|
self.system = get_test_descriptor_system()
|
|
self.all_blocks = {}
|
|
self.system.get_block = self.all_blocks.get
|
|
self.field_data = InheritingFieldData(
|
|
inheritable_names=['inherited'],
|
|
kvs=DictKeyValueStore({}),
|
|
)
|
|
|
|
def get_block_using_split_kvs(self, block_type, block_id, fields, defaults):
|
|
"""
|
|
Construct an Xblock with split mongo kvs.
|
|
"""
|
|
kvs = SplitMongoKVS(
|
|
definition=Mock(),
|
|
initial_values=fields,
|
|
default_values=defaults,
|
|
parent=None
|
|
)
|
|
self.field_data = InheritingFieldData(
|
|
inheritable_names=['inherited'],
|
|
kvs=kvs,
|
|
)
|
|
block = self.get_a_block(
|
|
usage_id=self.get_usage_id(block_type, block_id)
|
|
)
|
|
|
|
return block
|
|
|
|
def get_a_block(self, usage_id=None):
|
|
"""
|
|
Construct an XBlock for testing with.
|
|
"""
|
|
scope_ids = Mock()
|
|
if usage_id is None:
|
|
block_id = f"_auto{len(self.all_blocks)}"
|
|
usage_id = self.get_usage_id("course", block_id)
|
|
scope_ids.usage_id = usage_id
|
|
block = self.system.construct_xblock_from_class(
|
|
self.TestableInheritingXBlock,
|
|
field_data=self.field_data,
|
|
scope_ids=scope_ids,
|
|
)
|
|
self.all_blocks[usage_id] = block
|
|
return block
|
|
|
|
def get_usage_id(self, block_type, block_id):
|
|
"""
|
|
Constructs usage id using 'block_type' and 'block_id'
|
|
"""
|
|
return BlockUsageLocator(self.dummy_course_key, block_type=block_type, block_id=block_id)
|
|
|
|
def test_default_value(self):
|
|
"""
|
|
Test that the Blocks with nothing set with return the fields' defaults.
|
|
"""
|
|
block = self.get_a_block()
|
|
assert block.inherited == 'the default'
|
|
assert block.not_inherited == 'nothing'
|
|
|
|
def test_set_value(self):
|
|
"""
|
|
Test that If you set a value, that's what you get back.
|
|
"""
|
|
block = self.get_a_block()
|
|
block.inherited = "Changed!"
|
|
block.not_inherited = "New Value!"
|
|
assert block.inherited == 'Changed!'
|
|
assert block.not_inherited == 'New Value!'
|
|
|
|
def test_inherited(self):
|
|
"""
|
|
Test that a child with get a value inherited from the parent.
|
|
"""
|
|
parent_block = self.get_a_block(usage_id=self.get_usage_id("course", "parent"))
|
|
parent_block.inherited = "Changed!"
|
|
assert parent_block.inherited == 'Changed!'
|
|
|
|
child = self.get_a_block(usage_id=self.get_usage_id("vertical", "child"))
|
|
child.parent = parent_block.location
|
|
assert child.inherited == 'Changed!'
|
|
|
|
def test_inherited_across_generations(self):
|
|
"""
|
|
Test that a child with get a value inherited from a great-grandparent.
|
|
"""
|
|
parent = self.get_a_block(usage_id=self.get_usage_id("course", "parent"))
|
|
parent.inherited = "Changed!"
|
|
assert parent.inherited == 'Changed!'
|
|
for child_num in range(10):
|
|
usage_id = self.get_usage_id("vertical", f"child_{child_num}")
|
|
child = self.get_a_block(usage_id=usage_id)
|
|
child.parent = parent.location
|
|
assert child.inherited == 'Changed!'
|
|
|
|
def test_not_inherited(self):
|
|
"""
|
|
Test that the fields not in the inherited_names list won't be inherited.
|
|
"""
|
|
parent = self.get_a_block(usage_id=self.get_usage_id("course", "parent"))
|
|
parent.not_inherited = "Changed!"
|
|
assert parent.not_inherited == 'Changed!'
|
|
|
|
child = self.get_a_block(usage_id=self.get_usage_id("vertical", "child"))
|
|
child.parent = parent.location
|
|
assert child.not_inherited == 'nothing'
|
|
|
|
def test_non_defaults_inherited_across_lib(self):
|
|
"""
|
|
Test that a child inheriting from library_content block, inherits fields
|
|
from parent if these fields are not in its defaults.
|
|
"""
|
|
parent_block = self.get_block_using_split_kvs(
|
|
block_type="library_content",
|
|
block_id="parent",
|
|
fields=dict(inherited="changed!"),
|
|
defaults=dict(inherited="parent's default"),
|
|
)
|
|
assert parent_block.inherited == 'changed!'
|
|
|
|
child = self.get_block_using_split_kvs(
|
|
block_type="problem",
|
|
block_id="child",
|
|
fields={},
|
|
defaults={},
|
|
)
|
|
child.parent = parent_block.location
|
|
assert child.inherited == 'changed!'
|
|
|
|
def test_defaults_not_inherited_across_lib(self):
|
|
"""
|
|
Test that a child inheriting from library_content block, does not inherit
|
|
fields from parent if these fields are in its defaults already.
|
|
"""
|
|
parent_block = self.get_block_using_split_kvs(
|
|
block_type="library_content",
|
|
block_id="parent",
|
|
fields=dict(inherited="changed!"),
|
|
defaults=dict(inherited="parent's default"),
|
|
)
|
|
assert parent_block.inherited == 'changed!'
|
|
|
|
child = self.get_block_using_split_kvs(
|
|
block_type="library_content",
|
|
block_id="parent",
|
|
fields={},
|
|
defaults=dict(inherited="child's default"),
|
|
)
|
|
child.parent = parent_block.location
|
|
assert child.inherited == "child's default"
|
|
|
|
|
|
class EditableMetadataFieldsTest(unittest.TestCase):
|
|
class TestableXmlXBlock(XmlMixin, XModuleMixin): # lint-amnesty, pylint: disable=abstract-method
|
|
"""
|
|
This is subclassing `XModuleMixin` to use metadata fields in the unmixed class.
|
|
"""
|
|
|
|
def test_display_name_field(self):
|
|
editable_fields = self.get_xml_editable_fields(DictFieldData({}))
|
|
# Tests that the xblock fields (currently tags and name) get filtered out.
|
|
# Also tests that xml_attributes is filtered out of XmlMixin.
|
|
assert 1 == len(editable_fields), editable_fields
|
|
self.assert_field_values(
|
|
editable_fields, 'display_name', XModuleMixin.display_name,
|
|
explicitly_set=False, value=None, default_value=None
|
|
)
|
|
|
|
def test_override_default(self):
|
|
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
|
|
editable_fields = self.get_xml_editable_fields(DictFieldData({'display_name': 'foo'}))
|
|
self.assert_field_values(
|
|
editable_fields, 'display_name', XModuleMixin.display_name,
|
|
explicitly_set=True, value='foo', default_value=None
|
|
)
|
|
|
|
def test_integer_field(self):
|
|
descriptor = self.get_descriptor(DictFieldData({'max_attempts': '7'}))
|
|
editable_fields = descriptor.editable_metadata_fields
|
|
assert 8 == len(editable_fields)
|
|
self.assert_field_values(
|
|
editable_fields, 'max_attempts', TestFields.max_attempts,
|
|
explicitly_set=True, value=7, default_value=1000, type='Integer',
|
|
options=TestFields.max_attempts.values
|
|
)
|
|
self.assert_field_values(
|
|
editable_fields, 'display_name', TestFields.display_name,
|
|
explicitly_set=False, value='local default', default_value='local default'
|
|
)
|
|
|
|
editable_fields = self.get_descriptor(DictFieldData({})).editable_metadata_fields
|
|
self.assert_field_values(
|
|
editable_fields, 'max_attempts', TestFields.max_attempts,
|
|
explicitly_set=False, value=1000, default_value=1000, type='Integer',
|
|
options=TestFields.max_attempts.values
|
|
)
|
|
|
|
def test_inherited_field(self):
|
|
kvs = InheritanceKeyValueStore(initial_values={}, inherited_settings={'showanswer': 'inherited'})
|
|
model_data = KvsFieldData(kvs)
|
|
descriptor = self.get_descriptor(model_data)
|
|
editable_fields = descriptor.editable_metadata_fields
|
|
self.assert_field_values(
|
|
editable_fields, 'showanswer', InheritanceMixin.showanswer,
|
|
explicitly_set=False, value='inherited', default_value='inherited'
|
|
)
|
|
|
|
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
|
|
kvs = InheritanceKeyValueStore(
|
|
initial_values={'showanswer': 'explicit'},
|
|
inherited_settings={'showanswer': 'inheritable value'}
|
|
)
|
|
model_data = KvsFieldData(kvs)
|
|
descriptor = self.get_descriptor(model_data)
|
|
editable_fields = descriptor.editable_metadata_fields
|
|
self.assert_field_values(
|
|
editable_fields, 'showanswer', InheritanceMixin.showanswer,
|
|
explicitly_set=True, value='explicit', default_value='inheritable value'
|
|
)
|
|
|
|
def test_type_and_options(self):
|
|
# test_display_name_field verifies that a String field is of type "Generic".
|
|
# test_integer_field verifies that a Integer field is of type "Integer".
|
|
|
|
descriptor = self.get_descriptor(DictFieldData({}))
|
|
editable_fields = descriptor.editable_metadata_fields
|
|
|
|
# Tests for select
|
|
self.assert_field_values(
|
|
editable_fields, 'string_select', TestFields.string_select,
|
|
explicitly_set=False, value='default value', default_value='default value',
|
|
type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
|
|
{'display_name': 'second', 'value': 'value b JSON'}]
|
|
)
|
|
|
|
self.assert_field_values(
|
|
editable_fields, 'float_select', TestFields.float_select,
|
|
explicitly_set=False, value=.999, default_value=.999,
|
|
type='Select', options=[1.23, 0.98]
|
|
)
|
|
|
|
self.assert_field_values(
|
|
editable_fields, 'boolean_select', TestFields.boolean_select,
|
|
explicitly_set=False, value=None, default_value=None,
|
|
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
|
|
)
|
|
|
|
# Test for float
|
|
self.assert_field_values(
|
|
editable_fields, 'float_non_select', TestFields.float_non_select,
|
|
explicitly_set=False, value=.999, default_value=.999,
|
|
type='Float', options={'min': 0, 'step': .3}
|
|
)
|
|
|
|
self.assert_field_values(
|
|
editable_fields, 'list_field', TestFields.list_field,
|
|
explicitly_set=False, value=[], default_value=[],
|
|
type='List'
|
|
)
|
|
|
|
# Start of helper methods
|
|
def get_xml_editable_fields(self, field_data):
|
|
runtime = get_test_descriptor_system()
|
|
return runtime.construct_xblock_from_class(
|
|
self.TestableXmlXBlock,
|
|
scope_ids=Mock(),
|
|
field_data=field_data,
|
|
).editable_metadata_fields
|
|
|
|
def get_descriptor(self, field_data):
|
|
class TestModuleDescriptor(TestFields, self.TestableXmlXBlock): # lint-amnesty, pylint: disable=abstract-method
|
|
@property
|
|
def non_editable_metadata_fields(self):
|
|
non_editable_fields = super().non_editable_metadata_fields
|
|
non_editable_fields.append(TestModuleDescriptor.due)
|
|
return non_editable_fields
|
|
|
|
system = get_test_descriptor_system(render_template=Mock())
|
|
return system.construct_xblock_from_class(TestModuleDescriptor, field_data=field_data, scope_ids=Mock())
|
|
|
|
def assert_field_values(self, editable_fields, name, field, explicitly_set, value, default_value, # lint-amnesty, pylint: disable=dangerous-default-value
|
|
type='Generic', options=[]): # lint-amnesty, pylint: disable=redefined-builtin
|
|
test_field = editable_fields[name]
|
|
|
|
assert field.name == test_field['field_name']
|
|
assert field.display_name == test_field['display_name']
|
|
assert field.help == test_field['help']
|
|
|
|
assert field.to_json(value) == test_field['value']
|
|
assert field.to_json(default_value) == test_field['default_value']
|
|
|
|
assert options == test_field['options']
|
|
assert type == test_field['type']
|
|
|
|
assert explicitly_set == test_field['explicitly_set']
|
|
|
|
|
|
class TestSerialize(unittest.TestCase):
|
|
""" Tests the serialize, method, which is not dependent on type. """
|
|
|
|
def test_serialize(self):
|
|
assert serialize_field(None) == 'null'
|
|
assert serialize_field(-2) == '-2'
|
|
assert serialize_field('2') == '2'
|
|
assert serialize_field(-3.41) == '-3.41'
|
|
assert serialize_field('2.589') == '2.589'
|
|
assert serialize_field(False) == 'false'
|
|
assert serialize_field('false') == 'false'
|
|
assert serialize_field('fAlse') == 'fAlse'
|
|
assert serialize_field('hat box') == 'hat box'
|
|
serialized_dict = serialize_field({'bar': 'hat', 'frog': 'green'})
|
|
assert serialized_dict == '{"bar": "hat", "frog": "green"}' or serialized_dict == '{"frog": "green", "bar": "hat"}' # lint-amnesty, pylint: disable=consider-using-in, line-too-long
|
|
assert serialize_field([3.5, 5.6]) == '[3.5, 5.6]'
|
|
assert serialize_field(['foo', 'bar']) == '["foo", "bar"]'
|
|
assert serialize_field("2012-12-31T23:59:59Z") == '2012-12-31T23:59:59Z'
|
|
assert serialize_field("1 day 12 hours 59 minutes 59 seconds") == '1 day 12 hours 59 minutes 59 seconds'
|
|
assert serialize_field(dateutil.parser.parse('2012-12-31T23:59:59Z')) == '2012-12-31T23:59:59+00:00'
|
|
|
|
|
|
class TestDeserialize(unittest.TestCase):
|
|
|
|
def assertDeserializeEqual(self, expected, arg):
|
|
"""
|
|
Asserts the result of deserialize_field.
|
|
"""
|
|
assert deserialize_field(self.field_type(), arg) == expected # lint-amnesty, pylint: disable=no-member
|
|
|
|
def assertDeserializeNonString(self):
|
|
"""
|
|
Asserts input value is returned for None or something that is not a string.
|
|
For all types, 'null' is also always returned as None.
|
|
"""
|
|
self.assertDeserializeEqual(None, None)
|
|
self.assertDeserializeEqual(3.14, 3.14)
|
|
self.assertDeserializeEqual(True, True)
|
|
self.assertDeserializeEqual([10], [10])
|
|
self.assertDeserializeEqual({}, {})
|
|
self.assertDeserializeEqual([], [])
|
|
self.assertDeserializeEqual(None, 'null')
|
|
|
|
|
|
class TestDeserializeInteger(TestDeserialize):
|
|
""" Tests deserialize as related to Integer type. """
|
|
|
|
field_type = Integer
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual(-2, '-2')
|
|
self.assertDeserializeEqual("450", '"450"')
|
|
|
|
# False can be parsed as a int (converts to 0)
|
|
self.assertDeserializeEqual(False, 'false')
|
|
# True can be parsed as a int (converts to 1)
|
|
self.assertDeserializeEqual(True, 'true')
|
|
# 2.78 can be converted to int, so the string will be deserialized
|
|
self.assertDeserializeEqual(-2.78, '-2.78')
|
|
|
|
def test_deserialize_unsupported_types(self):
|
|
self.assertDeserializeEqual('[3]', '[3]')
|
|
# '2.78' cannot be converted to int, so input value is returned
|
|
self.assertDeserializeEqual('"-2.78"', '"-2.78"')
|
|
# 'false' cannot be converted to int, so input value is returned
|
|
self.assertDeserializeEqual('"false"', '"false"')
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeFloat(TestDeserialize):
|
|
""" Tests deserialize as related to Float type. """
|
|
|
|
field_type = Float
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual(-2, '-2')
|
|
self.assertDeserializeEqual("450", '"450"')
|
|
self.assertDeserializeEqual(-2.78, '-2.78')
|
|
self.assertDeserializeEqual("0.45", '"0.45"')
|
|
|
|
# False can be parsed as a float (converts to 0)
|
|
self.assertDeserializeEqual(False, 'false')
|
|
# True can be parsed as a float (converts to 1)
|
|
self.assertDeserializeEqual(True, 'true')
|
|
|
|
def test_deserialize_unsupported_types(self):
|
|
self.assertDeserializeEqual('[3]', '[3]')
|
|
# 'false' cannot be converted to float, so input value is returned
|
|
self.assertDeserializeEqual('"false"', '"false"')
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeBoolean(TestDeserialize):
|
|
""" Tests deserialize as related to Boolean type. """
|
|
|
|
field_type = Boolean
|
|
|
|
def test_deserialize(self):
|
|
# json.loads converts the value to Python bool
|
|
self.assertDeserializeEqual(False, 'false')
|
|
self.assertDeserializeEqual(True, 'true')
|
|
|
|
# json.loads fails, string value is returned.
|
|
self.assertDeserializeEqual('False', 'False')
|
|
self.assertDeserializeEqual('True', 'True')
|
|
|
|
# json.loads deserializes as a string
|
|
self.assertDeserializeEqual('false', '"false"')
|
|
self.assertDeserializeEqual('fAlse', '"fAlse"')
|
|
self.assertDeserializeEqual("TruE", '"TruE"')
|
|
|
|
# 2.78 can be converted to a bool, so the string will be deserialized
|
|
self.assertDeserializeEqual(-2.78, '-2.78')
|
|
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeString(TestDeserialize):
|
|
""" Tests deserialize as related to String type. """
|
|
|
|
field_type = String
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual('hAlf', '"hAlf"')
|
|
self.assertDeserializeEqual('false', '"false"')
|
|
self.assertDeserializeEqual('single quote', 'single quote')
|
|
|
|
def test_deserialize_unsupported_types(self):
|
|
self.assertDeserializeEqual('3.4', '3.4')
|
|
self.assertDeserializeEqual('false', 'false')
|
|
self.assertDeserializeEqual('2', '2')
|
|
self.assertDeserializeEqual('[3]', '[3]')
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeAny(TestDeserialize):
|
|
""" Tests deserialize as related to Any type. """
|
|
|
|
field_type = Any
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual('hAlf', '"hAlf"')
|
|
self.assertDeserializeEqual('false', '"false"')
|
|
self.assertDeserializeEqual({'bar': 'hat', 'frog': 'green'}, '{"bar": "hat", "frog": "green"}')
|
|
self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
|
|
self.assertDeserializeEqual('[', '[')
|
|
self.assertDeserializeEqual(False, 'false')
|
|
self.assertDeserializeEqual(3.4, '3.4')
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeList(TestDeserialize):
|
|
""" Tests deserialize as related to List type. """
|
|
|
|
field_type = List
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual(['foo', 'bar'], '["foo", "bar"]')
|
|
self.assertDeserializeEqual([3.5, 5.6], '[3.5, 5.6]')
|
|
self.assertDeserializeEqual([], '[]')
|
|
|
|
def test_deserialize_unsupported_types(self):
|
|
self.assertDeserializeEqual('3.4', '3.4')
|
|
self.assertDeserializeEqual('false', 'false')
|
|
self.assertDeserializeEqual('2', '2')
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeDate(TestDeserialize):
|
|
""" Tests deserialize as related to Date type. """
|
|
|
|
field_type = Date
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual('2012-12-31T23:59:59Z', "2012-12-31T23:59:59Z")
|
|
self.assertDeserializeEqual('2012-12-31T23:59:59Z', '"2012-12-31T23:59:59Z"')
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeTimedelta(TestDeserialize):
|
|
""" Tests deserialize as related to Timedelta type. """
|
|
|
|
field_type = Timedelta
|
|
|
|
def test_deserialize(self):
|
|
self.assertDeserializeEqual(
|
|
'1 day 12 hours 59 minutes 59 seconds',
|
|
'1 day 12 hours 59 minutes 59 seconds'
|
|
)
|
|
self.assertDeserializeEqual(
|
|
'1 day 12 hours 59 minutes 59 seconds',
|
|
'"1 day 12 hours 59 minutes 59 seconds"'
|
|
)
|
|
self.assertDeserializeNonString()
|
|
|
|
|
|
class TestDeserializeRelativeTime(TestDeserialize):
|
|
""" Tests deserialize as related to Timedelta type. """
|
|
|
|
field_type = RelativeTime
|
|
|
|
def test_deserialize(self):
|
|
"""
|
|
There is no check for
|
|
|
|
self.assertDeserializeEqual('10:20:30', '10:20:30')
|
|
self.assertDeserializeNonString()
|
|
|
|
because these two tests work only because json.loads fires exception,
|
|
and xml_module.deserialized_field catches it and returns same value,
|
|
so there is nothing field-specific here.
|
|
But other modules do it, so I'm leaving this comment for PR reviewers.
|
|
"""
|
|
|
|
# test that from_json produces no exceptions
|
|
self.assertDeserializeEqual('10:20:30', '"10:20:30"')
|
|
|
|
|
|
class TestXmlAttributes(XModuleXmlImportTest):
|
|
|
|
def test_unknown_attribute(self):
|
|
assert not hasattr(CourseBlock, 'unknown_attr')
|
|
course = self.process_xml(CourseFactory.build(unknown_attr='value'))
|
|
assert not hasattr(course, 'unknown_attr')
|
|
assert course.xml_attributes['unknown_attr'] == 'value'
|
|
|
|
def test_known_attribute(self):
|
|
assert hasattr(CourseBlock, 'show_calculator')
|
|
course = self.process_xml(CourseFactory.build(show_calculator='true'))
|
|
assert course.show_calculator
|
|
assert 'show_calculator' not in course.xml_attributes
|
|
|
|
def test_rerandomize_in_policy(self):
|
|
# Rerandomize isn't a basic attribute of Sequence
|
|
assert not hasattr(SequenceBlock, 'rerandomize')
|
|
|
|
root = SequenceFactory.build(policy={'rerandomize': 'never'})
|
|
ProblemFactory.build(parent=root)
|
|
|
|
seq = self.process_xml(root)
|
|
|
|
# Rerandomize is added to the constructed sequence via the InheritanceMixin
|
|
assert seq.rerandomize == 'never'
|
|
|
|
# Rerandomize is a known value coming from policy, and shouldn't appear
|
|
# in xml_attributes
|
|
assert 'rerandomize' not in seq.xml_attributes
|
|
|
|
def test_attempts_in_policy(self):
|
|
# attempts isn't a basic attribute of Sequence
|
|
assert not hasattr(SequenceBlock, 'attempts')
|
|
|
|
root = SequenceFactory.build(policy={'attempts': '1'})
|
|
ProblemFactory.build(parent=root)
|
|
|
|
seq = self.process_xml(root)
|
|
|
|
# attempts isn't added to the constructed sequence, because
|
|
# it's not in the InheritanceMixin
|
|
assert not hasattr(seq, 'attempts')
|
|
|
|
# attempts is an unknown attribute, so we should include it
|
|
# in xml_attributes so that it gets written out (despite the misleading
|
|
# name)
|
|
assert 'attempts' in seq.xml_attributes
|
|
|
|
def check_inheritable_attribute(self, attribute, value):
|
|
# `attribute` isn't a basic attribute of Sequence
|
|
assert not hasattr(SequenceBlock, attribute)
|
|
|
|
# `attribute` is added by InheritanceMixin
|
|
assert hasattr(InheritanceMixin, attribute)
|
|
|
|
root = SequenceFactory.build(policy={attribute: str(value)})
|
|
ProblemFactory.build(parent=root)
|
|
|
|
# InheritanceMixin will be used when processing the XML
|
|
assert InheritanceMixin in root.xblock_mixins
|
|
|
|
seq = self.process_xml(root)
|
|
|
|
assert seq.unmixed_class == SequenceBlock
|
|
assert not seq.__class__ == SequenceBlock
|
|
|
|
# `attribute` is added to the constructed sequence, because
|
|
# it's in the InheritanceMixin
|
|
assert getattr(seq, attribute) == value
|
|
|
|
# `attribute` is a known attribute, so we shouldn't include it
|
|
# in xml_attributes
|
|
assert attribute not in seq.xml_attributes
|
|
|
|
def test_inheritable_attributes(self):
|
|
self.check_inheritable_attribute('days_early_for_beta', 2)
|
|
self.check_inheritable_attribute('max_attempts', 5)
|
|
self.check_inheritable_attribute('visible_to_staff_only', True)
|