Commit 52beec88 by Calen Pennington

Switch inheritance in split-mongo over to using InheritingFieldData.

parent 687708c3
...@@ -56,7 +56,7 @@ class ContentStoreImportTest(ModuleStoreTestCase): ...@@ -56,7 +56,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
target_course_id=target_course_id, target_course_id=target_course_id,
create_new_course_if_not_present=create_new_course_if_not_present, create_new_course_if_not_present=create_new_course_if_not_present,
) )
course_id = SlashSeparatedCourseKey('edX', 'test_import_course', '2012_Fall') course_id = module_store.make_course_key('edX', 'test_import_course', '2012_Fall')
course = module_store.get_course(course_id) course = module_store.get_course(course_id)
self.assertIsNotNone(course) self.assertIsNotNone(course)
......
...@@ -116,6 +116,9 @@ class Timedelta(JSONField): ...@@ -116,6 +116,9 @@ class Timedelta(JSONField):
return datetime.timedelta(**time_params) return datetime.timedelta(**time_params)
def to_json(self, value): def to_json(self, value):
if value is None:
return None
values = [] values = []
for attr in ('days', 'hours', 'minutes', 'seconds'): for attr in ('days', 'hours', 'minutes', 'seconds'):
cur_value = getattr(value, attr, 0) cur_value = getattr(value, attr, 0)
......
...@@ -214,11 +214,19 @@ class InheritingFieldData(KvsFieldData): ...@@ -214,11 +214,19 @@ class InheritingFieldData(KvsFieldData):
""" """
The default for an inheritable name is found on a parent. The default for an inheritable name is found on a parent.
""" """
if name in self.inheritable_names and block.parent is not None: if name in self.inheritable_names:
parent = block.get_parent() # Walk up the content tree to find the first ancestor
if parent: # that this field is set on. Use the field from the current
return getattr(parent, name) # block so that if it has a different default than the root
super(InheritingFieldData, self).default(block, name) # node of the tree, the block's default will be used.
field = block.fields[name]
ancestor = block.get_parent()
while ancestor is not None:
if field.is_set_on(ancestor):
return field.read_json(ancestor)
else:
ancestor = ancestor.get_parent()
return super(InheritingFieldData, self).default(block, name)
def inheriting_field_data(kvs): def inheriting_field_data(kvs):
......
import sys import sys
import logging import logging
from contracts import contract, new_contract from contracts import contract, new_contract
from lazy import lazy
from xblock.runtime import KvsFieldData from xblock.runtime import KvsFieldData
from xblock.fields import ScopeIds from xblock.fields import ScopeIds
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, DefinitionLocator from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, DefinitionLocator
...@@ -12,6 +13,7 @@ from .split_mongo_kvs import SplitMongoKVS ...@@ -12,6 +13,7 @@ from .split_mongo_kvs import SplitMongoKVS
from fs.osfs import OSFS from fs.osfs import OSFS
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.edit_info import EditInfoRuntimeMixin from xmodule.modulestore.edit_info import EditInfoRuntimeMixin
from xmodule.modulestore.inheritance import inheriting_field_data, InheritanceMixin
from xmodule.modulestore.split_mongo import BlockKey from xmodule.modulestore.split_mongo import BlockKey
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -34,8 +36,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -34,8 +36,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
modulestore: the module store that can be used to retrieve additional modulestore: the module store that can be used to retrieve additional
modules modules
course_entry: the originally fetched enveloped course_structure w/ branch and course id info course_entry: the originally fetched enveloped course_structure w/ branch and course id info.
plus a dictionary of cached inherited_settings indexed by (block_type, block_id) tuple.
Callers to _load_item provide an override but that function ignores the provided structure and Callers to _load_item provide an override but that function ignores the provided structure and
only looks at the branch and course id only looks at the branch and course id
...@@ -59,15 +60,18 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -59,15 +60,18 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
self.course_entry = course_entry self.course_entry = course_entry
self.lazy = lazy self.lazy = lazy
self.module_data = module_data self.module_data = module_data
# Compute inheritance
modulestore.inherit_settings(
course_entry['structure'].get('blocks', {}),
course_entry['structure'].get('root'),
course_entry.setdefault('inherited_settings', {}),
)
self.default_class = default_class self.default_class = default_class
self.local_modules = {} self.local_modules = {}
@lazy
@contract(returns="dict(BlockKey: BlockKey)")
def _parent_map(self):
parent_map = {}
for block_key, block in self.course_entry['structure']['blocks'].iteritems():
for child in block['fields'].get('children', []):
parent_map[child] = block_key
return parent_map
@contract(usage_key="BlockUsageLocator | BlockKey") @contract(usage_key="BlockUsageLocator | BlockKey")
def _load_item(self, usage_key, course_entry_override=None, **kwargs): def _load_item(self, usage_key, course_entry_override=None, **kwargs):
# usage_key is either a UsageKey or just the block_key. if a usage_key, # usage_key is either a UsageKey or just the block_key. if a usage_key,
...@@ -96,12 +100,15 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -96,12 +100,15 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
branch=course_info.get('branch'), branch=course_info.get('branch'),
) )
if course_entry_override:
structure_id = course_entry_override.get('_id')
else:
structure_id = self.course_entry.get('_id')
json_data = self.get_module_data(block_key, course_key) json_data = self.get_module_data(block_key, course_key)
class_ = self.load_block_type(json_data.get('block_type')) class_ = self.load_block_type(json_data.get('block_type'))
# pass None for inherited_settings to signal that it should get the settings from cache return self.xblock_from_json(class_, course_key, block_key, json_data, course_entry_override, **kwargs)
new_item = self.xblock_from_json(class_, course_key, block_key, json_data, None, course_entry_override, **kwargs)
return new_item
@contract(block_key=BlockKey, course_key=CourseLocator) @contract(block_key=BlockKey, course_key=CourseLocator)
def get_module_data(self, block_key, course_key): def get_module_data(self, block_key, course_key):
...@@ -134,7 +141,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -134,7 +141,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
# is the intended one when not given a course_entry_override; thus, the caching of the last branch/course id. # is the intended one when not given a course_entry_override; thus, the caching of the last branch/course id.
@contract(block_key="BlockKey | None") @contract(block_key="BlockKey | None")
def xblock_from_json( def xblock_from_json(
self, class_, course_key, block_key, json_data, inherited_settings, course_entry_override=None, **kwargs self, class_, course_key, block_key, json_data, course_entry_override=None, **kwargs
): ):
if course_entry_override is None: if course_entry_override is None:
course_entry_override = self.course_entry course_entry_override = self.course_entry
...@@ -150,13 +157,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -150,13 +157,6 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
# If no usage id is provided, generate an in-memory id # If no usage id is provided, generate an in-memory id
if block_key is None: if block_key is None:
block_key = BlockKey(json_data['block_type'], LocalId()) block_key = BlockKey(json_data['block_type'], LocalId())
else:
if inherited_settings is None:
# see if there's a value in course_entry
if block_key in self.course_entry['inherited_settings']:
inherited_settings = self.course_entry['inherited_settings'][block_key]
elif block_key not in self.course_entry['inherited_settings']:
self.course_entry['inherited_settings'][block_key] = inherited_settings
if definition_id is not None and not json_data.get('definition_loaded', False): if definition_id is not None and not json_data.get('definition_loaded', False):
definition_loader = DefinitionLazyLoader( definition_loader = DefinitionLazyLoader(
...@@ -182,12 +182,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin): ...@@ -182,12 +182,21 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
converted_fields = self.modulestore.convert_references_to_keys( converted_fields = self.modulestore.convert_references_to_keys(
block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry['structure']['blocks'], block_locator.course_key, class_, json_data.get('fields', {}), self.course_entry['structure']['blocks'],
) )
if block_key in self._parent_map:
parent_key = self._parent_map[block_key]
parent = course_key.make_usage_key(parent_key.type, parent_key.id)
else:
parent = None
kvs = SplitMongoKVS( kvs = SplitMongoKVS(
definition_loader, definition_loader,
converted_fields, converted_fields,
inherited_settings, parent=parent,
**kwargs field_decorator=kwargs.get('field_decorator')
) )
if InheritanceMixin in self.modulestore.xblock_mixins:
field_data = inheriting_field_data(kvs)
else:
field_data = KvsFieldData(kvs) field_data = KvsFieldData(kvs)
try: try:
......
...@@ -1582,7 +1582,6 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -1582,7 +1582,6 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
course_key, course_key,
BlockKey(block_type, block_id) if block_id else None, BlockKey(block_type, block_id) if block_id else None,
json_data, json_data,
inherited_settings,
**kwargs **kwargs
) )
for field_name, value in (fields or {}).iteritems(): for field_name, value in (fields or {}).iteritems():
......
import copy import copy
from contracts import contract, new_contract
from xblock.fields import Scope from xblock.fields import Scope
from collections import namedtuple from collections import namedtuple
from xblock.exceptions import InvalidScopeError from xblock.exceptions import InvalidScopeError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from xmodule.modulestore.inheritance import InheritanceKeyValueStore from xmodule.modulestore.inheritance import InheritanceKeyValueStore
from opaque_keys.edx.locator import BlockUsageLocator
# id is a BlockUsageLocator, def_id is the definition's guid # id is a BlockUsageLocator, def_id is the definition's guid
SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id') SplitMongoKVSid = namedtuple('SplitMongoKVSid', 'id, def_id')
new_contract('BlockUsageLocator', BlockUsageLocator)
class SplitMongoKVS(InheritanceKeyValueStore): class SplitMongoKVS(InheritanceKeyValueStore):
...@@ -15,22 +18,25 @@ class SplitMongoKVS(InheritanceKeyValueStore): ...@@ -15,22 +18,25 @@ class SplitMongoKVS(InheritanceKeyValueStore):
known to the MongoModuleStore (data, children, and metadata) known to the MongoModuleStore (data, children, and metadata)
""" """
def __init__(self, definition, initial_values, inherited_settings, **kwargs): @contract(parent="BlockUsageLocator | None")
def __init__(self, definition, initial_values, parent, field_decorator=None):
""" """
:param definition: either a lazyloader or definition id for the definition :param definition: either a lazyloader or definition id for the definition
:param initial_values: a dictionary of the locally set values :param initial_values: a dictionary of the locally set values
:param inherited_settings: the json value of each inheritable field from above this.
Note, local fields may override and disagree w/ this b/c this says what the value
should be if the field is undefined.
""" """
# deepcopy so that manipulations of fields does not pollute the source # deepcopy so that manipulations of fields does not pollute the source
super(SplitMongoKVS, self).__init__(copy.deepcopy(initial_values), inherited_settings) super(SplitMongoKVS, self).__init__(copy.deepcopy(initial_values))
self._definition = definition # either a DefinitionLazyLoader or the db id of the definition. self._definition = definition # either a DefinitionLazyLoader or the db id of the definition.
# if the db id, then the definition is presumed to be loaded into _fields # if the db id, then the definition is presumed to be loaded into _fields
# a decorator function for field values (to be called when a field is accessed) # a decorator function for field values (to be called when a field is accessed)
self.field_decorator = kwargs.get('field_decorator', lambda x: x) if field_decorator is None:
self.field_decorator = lambda x: x
else:
self.field_decorator = field_decorator
self.parent = parent
def get(self, key): def get(self, key):
...@@ -38,8 +44,7 @@ class SplitMongoKVS(InheritanceKeyValueStore): ...@@ -38,8 +44,7 @@ class SplitMongoKVS(InheritanceKeyValueStore):
if key.field_name not in self._fields: if key.field_name not in self._fields:
# parent undefined in editing runtime (I think) # parent undefined in editing runtime (I think)
if key.scope == Scope.parent: if key.scope == Scope.parent:
# see STUD-624. Right now copies MongoKeyValueStore.get's behavior of returning None return self.parent
return None
if key.scope == Scope.children: if key.scope == Scope.children:
# didn't find children in _fields; so, see if there's a default # didn't find children in _fields; so, see if there's a default
raise KeyError() raise KeyError()
......
...@@ -323,6 +323,7 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest): ...@@ -323,6 +323,7 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest):
self.exclude_field(None, 'wiki_slug') self.exclude_field(None, 'wiki_slug')
self.exclude_field(None, 'xml_attributes') self.exclude_field(None, 'xml_attributes')
self.exclude_field(None, 'parent')
self.ignore_asset_key('_id') self.ignore_asset_key('_id')
self.ignore_asset_key('uploadDate') self.ignore_asset_key('uploadDate')
self.ignore_asset_key('content_son') self.ignore_asset_key('content_son')
......
...@@ -33,11 +33,11 @@ from xmodule.modulestore.mixed import MixedModuleStore ...@@ -33,11 +33,11 @@ from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.modulestore.search import path_to_location from xmodule.modulestore.search import path_to_location
from xmodule.modulestore.tests.factories import check_mongo_calls from xmodule.modulestore.tests.factories import check_mongo_calls
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR, CourseComparisonTest
@ddt.ddt @ddt.ddt
class TestMixedModuleStore(unittest.TestCase): class TestMixedModuleStore(CourseComparisonTest):
""" """
Quasi-superclass which tests Location based apps against both split and mongo dbs (Locator and Quasi-superclass which tests Location based apps against both split and mongo dbs (Locator and
Location-based dbs) Location-based dbs)
...@@ -1047,7 +1047,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -1047,7 +1047,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.revert_to_published(self.vertical_x1a, self.user_id) self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_parent = self.store.get_item(self.vertical_x1a) reverted_parent = self.store.get_item(self.vertical_x1a)
self.assertEqual(vertical_children_num, len(published_parent.children)) self.assertEqual(vertical_children_num, len(published_parent.children))
self.assertEqual(reverted_parent, published_parent) self.assertBlocksEqualByFields(reverted_parent, published_parent)
self.assertFalse(self._has_changes(self.vertical_x1a)) self.assertFalse(self._has_changes(self.vertical_x1a))
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
...@@ -1082,7 +1082,8 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -1082,7 +1082,8 @@ class TestMixedModuleStore(unittest.TestCase):
orig_vertical = self.store.get_item(self.vertical_x1a) orig_vertical = self.store.get_item(self.vertical_x1a)
self.store.revert_to_published(self.vertical_x1a, self.user_id) self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_vertical = self.store.get_item(self.vertical_x1a) reverted_vertical = self.store.get_item(self.vertical_x1a)
self.assertEqual(orig_vertical, reverted_vertical)
self.assertBlocksEqualByFields(orig_vertical, reverted_vertical)
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
def test_revert_to_published_no_published(self, default_ms): def test_revert_to_published_no_published(self, default_ms):
......
...@@ -10,6 +10,7 @@ from contracts import contract ...@@ -10,6 +10,7 @@ from contracts import contract
from importlib import import_module from importlib import import_module
from path import path from path import path
from xblock.fields import Reference, ReferenceList, ReferenceValueDict
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ( from xmodule.modulestore.exceptions import (
...@@ -1756,12 +1757,26 @@ class TestPublish(SplitModuleTest): ...@@ -1756,12 +1757,26 @@ class TestPublish(SplitModuleTest):
for field in source.fields.values(): for field in source.fields.values():
if field.name == 'children': if field.name == 'children':
self._compare_children(field.read_from(source), field.read_from(pub_copy), unexpected_blocks) self._compare_children(field.read_from(source), field.read_from(pub_copy), unexpected_blocks)
elif isinstance(field, (Reference, ReferenceList, ReferenceValueDict)):
self.assertReferenceEqual(field.read_from(source), field.read_from(pub_copy))
else: else:
self.assertEqual(field.read_from(source), field.read_from(pub_copy)) self.assertEqual(field.read_from(source), field.read_from(pub_copy))
for unexp in unexpected_blocks: for unexp in unexpected_blocks:
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
modulestore().get_item(dest_course_loc.make_usage_key(unexp.type, unexp.id)) modulestore().get_item(dest_course_loc.make_usage_key(unexp.type, unexp.id))
def assertReferenceEqual(self, expected, actual):
if isinstance(expected, BlockUsageLocator):
expected = BlockKey.from_usage_key(expected)
actual = BlockKey.from_usage_key(actual)
elif isinstance(expected, list):
expected = [BlockKey.from_usage_key(key) for key in expected]
actual = [BlockKey.from_usage_key(key) for key in actual]
elif isinstance(expected, dict):
expected = {key: BlockKey.from_usage_key(val) for (key, val) in expected}
actual = {key: BlockKey.from_usage_key(val) for (key, val) in actual}
self.assertEqual(expected, actual)
@contract( @contract(
source_children="list(BlockUsageLocator)", source_children="list(BlockUsageLocator)",
dest_children="list(BlockUsageLocator)", dest_children="list(BlockUsageLocator)",
......
...@@ -42,6 +42,9 @@ class Group(namedtuple("Group", "id name")): ...@@ -42,6 +42,9 @@ class Group(namedtuple("Group", "id name")):
Raises TypeError if the value doesn't have the right keys. Raises TypeError if the value doesn't have the right keys.
""" """
if isinstance(value, Group):
return value
for key in ('id', 'name', 'version'): for key in ('id', 'name', 'version'):
if key not in value: if key not in value:
raise TypeError("Group dict {0} missing value key '{1}'".format( raise TypeError("Group dict {0} missing value key '{1}'".format(
...@@ -96,6 +99,9 @@ class UserPartition(namedtuple("UserPartition", "id name description groups")): ...@@ -96,6 +99,9 @@ class UserPartition(namedtuple("UserPartition", "id name description groups")):
Raises TypeError if the value doesn't have the right keys. Raises TypeError if the value doesn't have the right keys.
""" """
if isinstance(value, UserPartition):
return value
for key in ('id', 'name', 'description', 'version', 'groups'): for key in ('id', 'name', 'description', 'version', 'groups'):
if key not in value: if key not in value:
raise TypeError("UserPartition dict {0} missing value key '{1}'" raise TypeError("UserPartition dict {0} missing value key '{1}'"
......
...@@ -13,7 +13,9 @@ import pprint ...@@ -13,7 +13,9 @@ import pprint
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
from lazy import lazy
from mock import Mock from mock import Mock
from operator import attrgetter
from path import path from path import path
from xblock.field_data import DictFieldData from xblock.field_data import DictFieldData
...@@ -227,6 +229,26 @@ class BulkAssertionTest(unittest.TestCase): ...@@ -227,6 +229,26 @@ class BulkAssertionTest(unittest.TestCase):
assertEquals = assertEqual assertEquals = assertEqual
class LazyFormat(object):
"""
An stringy object that delays formatting until it's put into a string context.
"""
__slots__ = ('template', 'args', 'kwargs', '_message')
def __init__(self, template, *args, **kwargs):
self.template = template
self.args = args
self.kwargs = kwargs
self._message = None
def __unicode__(self):
if self._message is None:
self._message = self.template.format(*self.args, **self.kwargs)
return self._message
def __repr__(self):
return unicode(self)
class CourseComparisonTest(BulkAssertionTest): class CourseComparisonTest(BulkAssertionTest):
""" """
Mixin that has methods for comparing courses for equality. Mixin that has methods for comparing courses for equality.
...@@ -256,6 +278,65 @@ class CourseComparisonTest(BulkAssertionTest): ...@@ -256,6 +278,65 @@ class CourseComparisonTest(BulkAssertionTest):
""" """
self.ignored_asset_keys.add(key_name) self.ignored_asset_keys.add(key_name)
def assertReferenceRelativelyEqual(self, reference_field, expected_block, actual_block):
"""
Assert that the supplied reference field is identical on the expected_block and actual_block,
assoming that the references are only relative (that is, comparing only on block_type and block_id,
not course_key).
"""
def extract_key(usage_key):
if usage_key is None:
return None
else:
return (usage_key.block_type, usage_key.block_id)
expected = reference_field.read_from(expected_block)
actual = reference_field.read_from(actual_block)
if isinstance(reference_field, Reference):
expected = extract_key(expected)
actual = extract_key(actual)
elif isinstance(reference_field, ReferenceList):
expected = [extract_key(key) for key in expected]
actual = [extract_key(key) for key in actual]
elif isinstance(reference_field, ReferenceValueDict):
expected = {key: extract_key(val) for (key, val) in expected.iteritems()}
actual = {key: extract_key(val) for (key, val) in actual.iteritems()}
self.assertEqual(
expected,
actual,
LazyFormat(
"Field {} doesn't match between usages {} and {}: {!r} != {!r}",
reference_field.name,
expected_block.scope_ids.usage_id,
actual_block.scope_ids.usage_id,
expected,
actual
)
)
def assertBlocksEqualByFields(self, expected_block, actual_block):
self.assertEqual(expected_block.fields, actual_block.fields)
for field in expected_block.fields.values():
self.assertFieldEqual(field, expected_block, actual_block)
def assertFieldEqual(self, field, expected_block, actual_block):
if isinstance(field, (Reference, ReferenceList, ReferenceValueDict)):
self.assertReferenceRelativelyEqual(field, expected_block, actual_block)
else:
expected = field.read_from(expected_block)
actual = field.read_from(actual_block)
self.assertEqual(
expected,
actual,
LazyFormat(
"Field {} doesn't match between usages {} and {}: {!r} != {!r}",
field.name,
expected_block.scope_ids.usage_id,
actual_block.scope_ids.usage_id,
expected,
actual
)
)
def assertCoursesEqual(self, expected_store, expected_course_key, actual_store, actual_course_key): def assertCoursesEqual(self, expected_store, expected_course_key, actual_store, actual_course_key):
""" """
Assert that the courses identified by ``expected_course_key`` in ``expected_store`` and Assert that the courses identified by ``expected_course_key`` in ``expected_store`` and
...@@ -313,11 +394,7 @@ class CourseComparisonTest(BulkAssertionTest): ...@@ -313,11 +394,7 @@ class CourseComparisonTest(BulkAssertionTest):
actual_item = actual_item_map.get(map_key(actual_item_location)) actual_item = actual_item_map.get(map_key(actual_item_location))
# Formatting the message slows down tests of large courses significantly, so only do it if it would be used # Formatting the message slows down tests of large courses significantly, so only do it if it would be used
if actual_item is None: self.assertIsNotNone(actual_item, LazyFormat(u'cannot find {} in {}', map_key(actual_item_location), actual_item_map))
msg = u'cannot find {} in {}'.format(map_key(actual_item_location), actual_item_map)
else:
msg = None
self.assertIsNotNone(actual_item, msg)
# compare fields # compare fields
self.assertEqual(expected_item.fields, actual_item.fields) self.assertEqual(expected_item.fields, actual_item.fields)
...@@ -333,20 +410,7 @@ class CourseComparisonTest(BulkAssertionTest): ...@@ -333,20 +410,7 @@ class CourseComparisonTest(BulkAssertionTest):
if field_name == 'children': if field_name == 'children':
continue continue
exp_value = map_references(field.read_from(expected_item), field, actual_course_key) self.assertFieldEqual(field, expected_item, actual_item)
actual_value = field.read_from(actual_item)
# Formatting the message slows down tests of large courses significantly, so only do it if it would be used
if exp_value != actual_value:
msg = "Field {!r} doesn't match between usages {} and {}: {!r} != {!r}".format(
field_name,
expected_item.scope_ids.usage_id,
actual_item.scope_ids.usage_id,
exp_value,
actual_value,
)
else:
msg = None
self.assertEqual(exp_value, actual_value, msg)
# compare children # compare children
self.assertEqual(expected_item.has_children, actual_item.has_children) self.assertEqual(expected_item.has_children, actual_item.has_children)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment