Commit 17d892c5 by jsa Committed by Andy Armstrong

make block.get_parent() work.

Co-Authored-By: Christina Roberts <christina@edx.org>
Co-Authored-By: Daniel Friedman <dfriedman@edx.org>
Co-Authored-By: Don Mitchell <dmitchell@edx.org>
parent 37d965d6
...@@ -143,7 +143,8 @@ def xblock_handler(request, usage_key_string): ...@@ -143,7 +143,8 @@ def xblock_handler(request, usage_key_string):
# right now can't combine output of this w/ output of _get_module_info, but worthy goal # right now can't combine output of this w/ output of _get_module_info, but worthy goal
return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key)) return JsonResponse(CourseGradingModel.get_section_grader_type(usage_key))
# TODO: pass fields to _get_module_info and only return those # TODO: pass fields to _get_module_info and only return those
rsp = _get_module_info(_get_xblock(usage_key, request.user)) with modulestore().bulk_operations(usage_key.course_key):
rsp = _get_module_info(_get_xblock(usage_key, request.user))
return JsonResponse(rsp) return JsonResponse(rsp)
else: else:
return HttpResponse(status=406) return HttpResponse(status=406)
......
...@@ -116,9 +116,20 @@ class GetItemTest(ItemTest): ...@@ -116,9 +116,20 @@ class GetItemTest(ItemTest):
return resp return resp
@ddt.data( @ddt.data(
(1, 21, 23, 35, 37), # chapter explanation:
(2, 22, 24, 38, 39), # 1-3. get course, chapter, chapter's children,
(3, 23, 25, 41, 41), # 4-7. chapter's published grandchildren, chapter's draft grandchildren, published & then draft greatgrand
# 8 compute chapter's parent
# 9 get chapter's parent
# 10-16. run queries 2-8 again
# 17-19. compute seq, vert, and problem's parents (odd since it's going down; so, it knows)
# 20-22. get course 3 times
# 23. get chapter
# 24. compute chapter's parent (course)
# 25. compute course's parent (None)
(1, 20, 20, 26, 26),
(2, 21, 21, 29, 28),
(3, 22, 22, 32, 30),
) )
@ddt.unpack @ddt.unpack
def test_get_query_count(self, branching_factor, chapter_queries, section_queries, unit_queries, problem_queries): def test_get_query_count(self, branching_factor, chapter_queries, section_queries, unit_queries, problem_queries):
...@@ -411,21 +422,46 @@ class TestDuplicateItem(ItemTest): ...@@ -411,21 +422,46 @@ class TestDuplicateItem(ItemTest):
except for location and display name. except for location and display name.
""" """
def duplicate_and_verify(source_usage_key, parent_usage_key): def duplicate_and_verify(source_usage_key, parent_usage_key):
""" Duplicates the source, parenting to supplied parent. Then does equality check. """
usage_key = self._duplicate_item(parent_usage_key, source_usage_key) usage_key = self._duplicate_item(parent_usage_key, source_usage_key)
self.assertTrue(check_equality(source_usage_key, usage_key), "Duplicated item differs from original") self.assertTrue(
check_equality(source_usage_key, usage_key, parent_usage_key),
"Duplicated item differs from original"
)
def check_equality(source_usage_key, duplicate_usage_key): def check_equality(source_usage_key, duplicate_usage_key, parent_usage_key=None):
"""
Gets source and duplicated items from the modulestore using supplied usage keys.
Then verifies that they represent equivalent items (modulo parents and other
known things that may differ).
"""
original_item = self.get_item_from_modulestore(source_usage_key) original_item = self.get_item_from_modulestore(source_usage_key)
duplicated_item = self.get_item_from_modulestore(duplicate_usage_key) duplicated_item = self.get_item_from_modulestore(duplicate_usage_key)
self.assertNotEqual( self.assertNotEqual(
original_item.location, unicode(original_item.location),
duplicated_item.location, unicode(duplicated_item.location),
"Location of duplicate should be different from original" "Location of duplicate should be different from original"
) )
# Set the location and display name to be the same so we can make sure the rest of the duplicate is equal.
# Parent will only be equal for root of duplicated structure, in the case
# where an item is duplicated in-place.
if parent_usage_key and unicode(original_item.parent) == unicode(parent_usage_key):
self.assertEqual(
unicode(parent_usage_key), unicode(duplicated_item.parent),
"Parent of duplicate should equal parent of source for root xblock when duplicated in-place"
)
else:
self.assertNotEqual(
unicode(original_item.parent), unicode(duplicated_item.parent),
"Parent duplicate should be different from source"
)
# Set the location, display name, and parent to be the same so we can make sure the rest of the
# duplicate is equal.
duplicated_item.location = original_item.location duplicated_item.location = original_item.location
duplicated_item.display_name = original_item.display_name duplicated_item.display_name = original_item.display_name
duplicated_item.parent = original_item.parent
# Children will also be duplicated, so for the purposes of testing equality, we will set # Children will also be duplicated, so for the purposes of testing equality, we will set
# the children to the original after recursively checking the children. # the children to the original after recursively checking the children.
......
...@@ -98,6 +98,7 @@ def update_module_store_settings( ...@@ -98,6 +98,7 @@ def update_module_store_settings(
module_store_options=None, module_store_options=None,
xml_store_options=None, xml_store_options=None,
default_store=None, default_store=None,
mappings=None,
): ):
""" """
Updates the settings for each store defined in the given module_store_setting settings Updates the settings for each store defined in the given module_store_setting settings
...@@ -123,6 +124,9 @@ def update_module_store_settings( ...@@ -123,6 +124,9 @@ def update_module_store_settings(
return return
raise Exception("Could not find setting for requested default store: {}".format(default_store)) raise Exception("Could not find setting for requested default store: {}".format(default_store))
if mappings and 'mappings' in module_store_setting['default']['OPTIONS']:
module_store_setting['default']['OPTIONS']['mappings'] = mappings
def get_mixed_stores(mixed_setting): def get_mixed_stores(mixed_setting):
""" """
......
...@@ -643,6 +643,9 @@ class DraftModuleStore(MongoModuleStore): ...@@ -643,6 +643,9 @@ class DraftModuleStore(MongoModuleStore):
Raises: Raises:
ItemNotFoundError: if any of the draft subtree nodes aren't found ItemNotFoundError: if any of the draft subtree nodes aren't found
Returns:
The newly published xblock
""" """
# NOTE: cannot easily use self._breadth_first b/c need to get pub'd and draft as pairs # NOTE: cannot easily use self._breadth_first b/c need to get pub'd and draft as pairs
# (could do it by having 2 breadth first scans, the first to just get all published children # (could do it by having 2 breadth first scans, the first to just get all published children
......
...@@ -210,26 +210,18 @@ class ItemFactory(XModuleFactory): ...@@ -210,26 +210,18 @@ class ItemFactory(XModuleFactory):
# replace the display name with an optional parameter passed in from the caller # replace the display name with an optional parameter passed in from the caller
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
runtime = parent.runtime if parent else None
store.create_item( module = store.create_child(
user_id, user_id,
location.course_key, parent.location,
location.block_type, location.block_type,
block_id=location.block_id, block_id=location.block_id,
metadata=metadata, metadata=metadata,
definition_data=data, definition_data=data,
runtime=runtime runtime=parent.runtime,
fields=kwargs,
) )
module = store.get_item(location)
for attr, val in kwargs.items():
setattr(module, attr, val)
# Save the attributes we just set
module.save()
store.update_item(module, user_id)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata) # if we add one then we need to also add it to the policy information (i.e. metadata)
# we should remove this once we can break this reference from the course to static tabs # we should remove this once we can break this reference from the course to static tabs
...@@ -248,12 +240,15 @@ class ItemFactory(XModuleFactory): ...@@ -248,12 +240,15 @@ class ItemFactory(XModuleFactory):
parent.children.append(location) parent.children.append(location)
store.update_item(parent, user_id) store.update_item(parent, user_id)
if publish_item: if publish_item:
store.publish(parent.location, user_id) published_parent = store.publish(parent.location, user_id)
# module is last child of parent
return published_parent.get_children()[-1]
else:
return store.get_item(location)
elif publish_item: elif publish_item:
store.publish(location, user_id) return store.publish(location, user_id)
else:
# return the published item return module
return store.get_item(location)
@contextmanager @contextmanager
......
...@@ -358,12 +358,12 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -358,12 +358,12 @@ class TestMixedModuleStore(CourseComparisonTest):
self.store.has_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred) self.store.has_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
# draft queries: # draft queries:
# problem: find draft item, find all items pertinent to inheritance computation # problem: find draft item, find all items pertinent to inheritance computation, find parent
# non-existent problem: find draft, find published # non-existent problem: find draft, find published
# split: # split:
# problem: active_versions, structure # problem: active_versions, structure
# non-existent problem: ditto # non-existent problem: ditto
@ddt.data(('draft', [2, 2], 0), ('split', [2, 2], 0)) @ddt.data(('draft', [3, 2], 0), ('split', [2, 2], 0))
@ddt.unpack @ddt.unpack
def test_get_item(self, default_ms, max_find, max_send): def test_get_item(self, default_ms, max_find, max_send):
self.initdb(default_ms) self.initdb(default_ms)
...@@ -388,10 +388,10 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -388,10 +388,10 @@ class TestMixedModuleStore(CourseComparisonTest):
self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred) self.store.get_item(self.fake_location, revision=ModuleStoreEnum.RevisionOption.draft_preferred)
# Draft: # Draft:
# wildcard query, 6! load pertinent items for inheritance calls, course root fetch (why) # wildcard query, 6! load pertinent items for inheritance calls, load parents, course root fetch (why)
# Split: # Split:
# active_versions (with regex), structure, and spurious active_versions refetch # active_versions (with regex), structure, and spurious active_versions refetch
@ddt.data(('draft', 8, 0), ('split', 3, 0)) @ddt.data(('draft', 14, 0), ('split', 3, 0))
@ddt.unpack @ddt.unpack
def test_get_items(self, default_ms, max_find, max_send): def test_get_items(self, default_ms, max_find, max_send):
self.initdb(default_ms) self.initdb(default_ms)
...@@ -405,7 +405,6 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -405,7 +405,6 @@ class TestMixedModuleStore(CourseComparisonTest):
course_locn = self.course_locations[self.MONGO_COURSEID] course_locn = self.course_locations[self.MONGO_COURSEID]
with check_mongo_calls(max_find, max_send): with check_mongo_calls(max_find, max_send):
# NOTE: use get_course if you just want the course. get_items is expensive
modules = self.store.get_items(course_locn.course_key, qualifiers={'category': 'problem'}) modules = self.store.get_items(course_locn.course_key, qualifiers={'category': 'problem'})
self.assertEqual(len(modules), 6) self.assertEqual(len(modules), 6)
...@@ -416,12 +415,11 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -416,12 +415,11 @@ class TestMixedModuleStore(CourseComparisonTest):
revision=ModuleStoreEnum.RevisionOption.draft_preferred revision=ModuleStoreEnum.RevisionOption.draft_preferred
) )
# draft: get draft, count parents, get parents, count & get grandparents, count & get greatgrand, # draft: get draft, get ancestors up to course (2-6), compute inheritance
# count & get next ancestor (chapter's parent), count non-existent next ancestor, get inheritance
# sends: update problem and then each ancestor up to course (edit info) # sends: update problem and then each ancestor up to course (edit info)
# split: active_versions, definitions (calculator field), structures # split: active_versions, definitions (calculator field), structures
# 2 sends to update index & structure (note, it would also be definition if a content field changed) # 2 sends to update index & structure (note, it would also be definition if a content field changed)
@ddt.data(('draft', 11, 5), ('split', 3, 2)) @ddt.data(('draft', 7, 5), ('split', 3, 2))
@ddt.unpack @ddt.unpack
def test_update_item(self, default_ms, max_find, max_send): def test_update_item(self, default_ms, max_find, max_send):
""" """
...@@ -886,9 +884,9 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -886,9 +884,9 @@ class TestMixedModuleStore(CourseComparisonTest):
# notice this doesn't test getting a public item via draft_preferred which draft would have 2 hits (split # notice this doesn't test getting a public item via draft_preferred which draft would have 2 hits (split
# still only 2) # still only 2)
# Draft: count via definition.children query, then fetch via that query # Draft: get_parent
# Split: active_versions, structure # Split: active_versions, structure
@ddt.data(('draft', 2, 0), ('split', 2, 0)) @ddt.data(('draft', 1, 0), ('split', 2, 0))
@ddt.unpack @ddt.unpack
def test_get_parent_locations(self, default_ms, max_find, max_send): def test_get_parent_locations(self, default_ms, max_find, max_send):
""" """
...@@ -1022,20 +1020,12 @@ class TestMixedModuleStore(CourseComparisonTest): ...@@ -1022,20 +1020,12 @@ class TestMixedModuleStore(CourseComparisonTest):
# Draft: # Draft:
# Problem path: # Problem path:
# 1. Get problem # 1. Get problem
# 2-3. count matches definition.children called 2x? # 2-6. get parent and rest of ancestors up to course
# 4. get parent via definition.children query # 7-8. get sequential, compute inheritance
# 5-7. 2 counts and 1 get grandparent via definition.children # 8-9. get vertical, compute inheritance
# 8-10. ditto for great-grandparent # 10-11. get other vertical_x1b (why?) and compute inheritance
# 11-13. ditto for next ancestor
# 14. fail count query looking for parent of course (unnecessary)
# 15. get course record direct query (not via definition.children) (already fetched in 13)
# 16. get items for inheritance computation
# 17. get vertical (parent of problem)
# 18. get items for inheritance computation (why? caching should handle)
# 19-20. get vertical_x1b (? why? this is the only ref in trace) & items for inheritance computation
# Chapter path: get chapter, count parents 2x, get parents, count non-existent grandparents
# Split: active_versions & structure # Split: active_versions & structure
@ddt.data(('draft', [20, 5], 0), ('split', [2, 2], 0)) @ddt.data(('draft', [12, 3], 0), ('split', [2, 2], 0))
@ddt.unpack @ddt.unpack
def test_path_to_location(self, default_ms, num_finds, num_sends): def test_path_to_location(self, default_ms, num_finds, num_sends):
""" """
......
...@@ -717,15 +717,16 @@ class TestMongoKeyValueStore(object): ...@@ -717,15 +717,16 @@ class TestMongoKeyValueStore(object):
def setUp(self): def setUp(self):
self.data = {'foo': 'foo_value'} self.data = {'foo': 'foo_value'}
self.course_id = SlashSeparatedCourseKey('org', 'course', 'run') self.course_id = SlashSeparatedCourseKey('org', 'course', 'run')
self.parent = self.course_id.make_usage_key('parent', 'p')
self.children = [self.course_id.make_usage_key('child', 'a'), self.course_id.make_usage_key('child', 'b')] self.children = [self.course_id.make_usage_key('child', 'a'), self.course_id.make_usage_key('child', 'b')]
self.metadata = {'meta': 'meta_val'} self.metadata = {'meta': 'meta_val'}
self.kvs = MongoKeyValueStore(self.data, self.children, self.metadata) self.kvs = MongoKeyValueStore(self.data, self.parent, self.children, self.metadata)
def test_read(self): def test_read(self):
assert_equals(self.data['foo'], self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'foo'))) assert_equals(self.data['foo'], self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'foo')))
assert_equals(self.parent, self.kvs.get(KeyValueStore.Key(Scope.parent, None, None, 'parent')))
assert_equals(self.children, self.kvs.get(KeyValueStore.Key(Scope.children, None, None, 'children'))) assert_equals(self.children, self.kvs.get(KeyValueStore.Key(Scope.children, None, None, 'children')))
assert_equals(self.metadata['meta'], self.kvs.get(KeyValueStore.Key(Scope.settings, None, None, 'meta'))) assert_equals(self.metadata['meta'], self.kvs.get(KeyValueStore.Key(Scope.settings, None, None, 'meta')))
assert_equals(None, self.kvs.get(KeyValueStore.Key(Scope.parent, None, None, 'parent')))
def test_read_invalid_scope(self): def test_read_invalid_scope(self):
for scope in (Scope.preferences, Scope.user_info, Scope.user_state): for scope in (Scope.preferences, Scope.user_info, Scope.user_state):
...@@ -735,7 +736,7 @@ class TestMongoKeyValueStore(object): ...@@ -735,7 +736,7 @@ class TestMongoKeyValueStore(object):
assert_false(self.kvs.has(key)) assert_false(self.kvs.has(key))
def test_read_non_dict_data(self): def test_read_non_dict_data(self):
self.kvs = MongoKeyValueStore('xml_data', self.children, self.metadata) self.kvs = MongoKeyValueStore('xml_data', self.parent, self.children, self.metadata)
assert_equals('xml_data', self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'data'))) assert_equals('xml_data', self.kvs.get(KeyValueStore.Key(Scope.content, None, None, 'data')))
def _check_write(self, key, value): def _check_write(self, key, value):
...@@ -746,9 +747,10 @@ class TestMongoKeyValueStore(object): ...@@ -746,9 +747,10 @@ class TestMongoKeyValueStore(object):
yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'foo'), 'new_data') yield (self._check_write, KeyValueStore.Key(Scope.content, None, None, 'foo'), 'new_data')
yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'children'), []) yield (self._check_write, KeyValueStore.Key(Scope.children, None, None, 'children'), [])
yield (self._check_write, KeyValueStore.Key(Scope.settings, None, None, 'meta'), 'new_settings') yield (self._check_write, KeyValueStore.Key(Scope.settings, None, None, 'meta'), 'new_settings')
# write Scope.parent raises InvalidScope, which is covered in test_write_invalid_scope
def test_write_non_dict_data(self): def test_write_non_dict_data(self):
self.kvs = MongoKeyValueStore('xml_data', self.children, self.metadata) self.kvs = MongoKeyValueStore('xml_data', self.parent, self.children, self.metadata)
self._check_write(KeyValueStore.Key(Scope.content, None, None, 'data'), 'new_data') self._check_write(KeyValueStore.Key(Scope.content, None, None, 'data'), 'new_data')
def test_write_invalid_scope(self): def test_write_invalid_scope(self):
......
...@@ -47,14 +47,10 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -47,14 +47,10 @@ class TestPublish(SplitWMongoCourseBoostrapper):
# For each (4) item created # For each (4) item created
# - try to find draft # - try to find draft
# - try to find non-draft # - try to find non-draft
# - retrieve draft of new parent # - compute what is parent
# - get last error # - load draft parent again & compute its parent chain up to course
# - load parent
# - load inheritable data
# - load parent
# - load ancestors
# count for updates increased to 16 b/c of edit_info updating # count for updates increased to 16 b/c of edit_info updating
with check_mongo_calls(40, 16): with check_mongo_calls(36, 16):
self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False) self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1', split=False)
self._create_item( self._create_item(
'discussion', 'Discussion1', 'discussion', 'Discussion1',
...@@ -96,22 +92,22 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -96,22 +92,22 @@ class TestPublish(SplitWMongoCourseBoostrapper):
item = self.draft_mongo.get_item(vert_location, 2) item = self.draft_mongo.get_item(vert_location, 2)
# Finds: # Finds:
# 1 get draft vert, # 1 get draft vert,
# 2-10 for each child: (3 children x 3 queries each) # 2 compute parent
# get draft and then published child # 3-14 for each child: (3 children x 4 queries each)
# get draft, compute parent, and then published child
# compute inheritance # compute inheritance
# 11 get published vert # 15 get published vert
# 12-15 get each ancestor (count then get): (2 x 2), # 16-18 get ancestor chain
# 16 then fail count of course parent (1) # 19 compute inheritance
# 17 compute inheritance # 20-22 get draft and published vert, compute parent
# 18-19 get draft and published vert
# Sends: # Sends:
# delete the subtree of drafts (1 call), # delete the subtree of drafts (1 call),
# update the published version of each node in subtree (4 calls), # update the published version of each node in subtree (4 calls),
# update the ancestors up to course (2 calls) # update the ancestors up to course (2 calls)
if mongo_uses_error_check(self.draft_mongo): if mongo_uses_error_check(self.draft_mongo):
max_find = 20 max_find = 23
else: else:
max_find = 19 max_find = 22
with check_mongo_calls(max_find, 7): with check_mongo_calls(max_find, 7):
self.draft_mongo.publish(item.location, self.user_id) self.draft_mongo.publish(item.location, self.user_id)
......
...@@ -31,7 +31,7 @@ class TestXMLModuleStore(unittest.TestCase): ...@@ -31,7 +31,7 @@ class TestXMLModuleStore(unittest.TestCase):
Test around the XML modulestore Test around the XML modulestore
""" """
def test_xml_modulestore_type(self): def test_xml_modulestore_type(self):
store = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple']) store = XMLModuleStore(DATA_DIR, course_dirs=[])
self.assertEqual(store.get_modulestore_type(), ModuleStoreEnum.Type.xml) self.assertEqual(store.get_modulestore_type(), ModuleStoreEnum.Type.xml)
def test_unicode_chars_in_xml_content(self): def test_unicode_chars_in_xml_content(self):
......
...@@ -53,7 +53,7 @@ def clean_out_mako_templating(xml_string): ...@@ -53,7 +53,7 @@ def clean_out_mako_templating(xml_string):
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
def __init__(self, xmlstore, course_id, course_dir, def __init__(self, xmlstore, course_id, course_dir,
error_tracker, parent_tracker, error_tracker,
load_error_modules=True, **kwargs): load_error_modules=True, **kwargs):
""" """
A class that handles loading from xml. Does some munging to ensure that A class that handles loading from xml. Does some munging to ensure that
...@@ -209,7 +209,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -209,7 +209,8 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
if descriptor.has_children: if descriptor.has_children:
for child in descriptor.get_children(): for child in descriptor.get_children():
parent_tracker.add_parent(child.scope_ids.usage_id, descriptor.scope_ids.usage_id) child.parent = descriptor.location
child.save()
# After setting up the descriptor, save any changes that we have # After setting up the descriptor, save any changes that we have
# made to attributes on the descriptor to the underlying KeyValueStore. # made to attributes on the descriptor to the underlying KeyValueStore.
...@@ -278,41 +279,6 @@ class CourseLocationManager(OpaqueKeyReader, AsideKeyGenerator): ...@@ -278,41 +279,6 @@ class CourseLocationManager(OpaqueKeyReader, AsideKeyGenerator):
return usage_id return usage_id
class ParentTracker(object):
"""A simple class to factor out the logic for tracking location parent pointers."""
def __init__(self):
"""
Init
"""
# location -> parent. Not using defaultdict because we care about the empty case.
self._parents = dict()
def add_parent(self, child, parent):
"""
Add a parent of child location to the set of parents. Duplicate calls have no effect.
child and parent must be :class:`.Location` instances.
"""
self._parents[child] = parent
def is_known(self, child):
"""
returns True iff child has some parents.
"""
return child in self._parents
def make_known(self, location):
"""Tell the parent tracker about an object, without registering any
parents for it. Used for the top level course descriptor locations."""
self._parents.setdefault(location, None)
def parent(self, child):
"""
Return the parent of this child. If not is_known(child), will throw a KeyError
"""
return self._parents[child]
class XMLModuleStore(ModuleStoreReadBase): class XMLModuleStore(ModuleStoreReadBase):
""" """
An XML backed ModuleStore An XML backed ModuleStore
...@@ -352,8 +318,6 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -352,8 +318,6 @@ class XMLModuleStore(ModuleStoreReadBase):
class_ = getattr(import_module(module_path), class_name) class_ = getattr(import_module(module_path), class_name)
self.default_class = class_ self.default_class = class_
self.parent_trackers = defaultdict(ParentTracker)
# All field data will be stored in an inheriting field data. # All field data will be stored in an inheriting field data.
self.field_data = inheriting_field_data(kvs=DictKeyValueStore()) self.field_data = inheriting_field_data(kvs=DictKeyValueStore())
...@@ -400,7 +364,7 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -400,7 +364,7 @@ class XMLModuleStore(ModuleStoreReadBase):
else: else:
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
self._course_errors[course_descriptor.id] = errorlog self._course_errors[course_descriptor.id] = errorlog
self.parent_trackers[course_descriptor.id].make_known(course_descriptor.scope_ids.usage_id) course_descriptor.parent = None
def __unicode__(self): def __unicode__(self):
''' '''
...@@ -512,7 +476,6 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -512,7 +476,6 @@ class XMLModuleStore(ModuleStoreReadBase):
course_id=course_id, course_id=course_id,
course_dir=course_dir, course_dir=course_dir,
error_tracker=tracker, error_tracker=tracker,
parent_tracker=self.parent_trackers[course_id],
load_error_modules=self.load_error_modules, load_error_modules=self.load_error_modules,
get_policy=get_policy, get_policy=get_policy,
mixins=self.xblock_mixins, mixins=self.xblock_mixins,
...@@ -756,10 +719,8 @@ class XMLModuleStore(ModuleStoreReadBase): ...@@ -756,10 +719,8 @@ class XMLModuleStore(ModuleStoreReadBase):
'''Find the location that is the parent of this location in this '''Find the location that is the parent of this location in this
course. Needed for path_to_location(). course. Needed for path_to_location().
''' '''
if not self.parent_trackers[location.course_key].is_known(location): block = self.get_item(location, 0)
raise ItemNotFoundError("{0} not in {1}".format(location, location.course_key)) return block.parent
return self.parent_trackers[location.course_key].parent(location)
def get_modulestore_type(self, course_key=None): def get_modulestore_type(self, course_key=None):
""" """
......
...@@ -28,7 +28,7 @@ import json ...@@ -28,7 +28,7 @@ import json
import re import re
from lxml import etree from lxml import etree
from .xml import XMLModuleStore, ImportSystem, ParentTracker from .xml import XMLModuleStore, ImportSystem
from xblock.runtime import KvsFieldData, DictKeyValueStore from xblock.runtime import KvsFieldData, DictKeyValueStore
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
...@@ -479,11 +479,13 @@ def _import_module_and_update_references( ...@@ -479,11 +479,13 @@ def _import_module_and_update_references(
fields = {} fields = {}
for field_name, field in module.fields.iteritems(): for field_name, field in module.fields.iteritems():
if field.is_set_on(module): if field.scope != Scope.parent and field.is_set_on(module):
if field.scope == Scope.parent:
continue
if isinstance(field, Reference): if isinstance(field, Reference):
fields[field_name] = _convert_reference_fields_to_new_namespace(field.read_from(module)) value = field.read_from(module)
if value is None:
fields[field_name] = None
else:
fields[field_name] = _convert_reference_fields_to_new_namespace(field.read_from(module))
elif isinstance(field, ReferenceList): elif isinstance(field, ReferenceList):
references = field.read_from(module) references = field.read_from(module)
fields[field_name] = [_convert_reference_fields_to_new_namespace(reference) for reference in references] fields[field_name] = [_convert_reference_fields_to_new_namespace(reference) for reference in references]
...@@ -548,7 +550,6 @@ def _import_course_draft( ...@@ -548,7 +550,6 @@ def _import_course_draft(
course_id=source_course_id, course_id=source_course_id,
course_dir=draft_course_dir, course_dir=draft_course_dir,
error_tracker=errorlog.tracker, error_tracker=errorlog.tracker,
parent_tracker=ParentTracker(),
load_error_modules=False, load_error_modules=False,
mixins=xml_module_store.xblock_mixins, mixins=xml_module_store.xblock_mixins,
field_data=KvsFieldData(kvs=DictKeyValueStore()), field_data=KvsFieldData(kvs=DictKeyValueStore()),
......
...@@ -30,7 +30,6 @@ class DummySystem(ImportSystem): ...@@ -30,7 +30,6 @@ class DummySystem(ImportSystem):
course_id=SlashSeparatedCourseKey(ORG, COURSE, 'test_run'), course_id=SlashSeparatedCourseKey(ORG, COURSE, 'test_run'),
course_dir='test_dir', course_dir='test_dir',
error_tracker=Mock(), error_tracker=Mock(),
parent_tracker=Mock(),
load_error_modules=load_error_modules, load_error_modules=load_error_modules,
) )
......
...@@ -36,14 +36,12 @@ class DummySystem(ImportSystem): ...@@ -36,14 +36,12 @@ class DummySystem(ImportSystem):
course_id = SlashSeparatedCourseKey(ORG, COURSE, 'test_run') course_id = SlashSeparatedCourseKey(ORG, COURSE, 'test_run')
course_dir = "test_dir" course_dir = "test_dir"
error_tracker = Mock() error_tracker = Mock()
parent_tracker = Mock()
super(DummySystem, self).__init__( super(DummySystem, self).__init__(
xmlstore=xmlstore, xmlstore=xmlstore,
course_id=course_id, course_id=course_id,
course_dir=course_dir, course_dir=course_dir,
error_tracker=error_tracker, error_tracker=error_tracker,
parent_tracker=parent_tracker,
load_error_modules=load_error_modules, load_error_modules=load_error_modules,
field_data=KvsFieldData(DictKeyValueStore()), field_data=KvsFieldData(DictKeyValueStore()),
) )
......
...@@ -39,14 +39,12 @@ class DummySystem(ImportSystem): ...@@ -39,14 +39,12 @@ class DummySystem(ImportSystem):
course_id = SlashSeparatedCourseKey(ORG, COURSE, 'test_run') course_id = SlashSeparatedCourseKey(ORG, COURSE, 'test_run')
course_dir = "test_dir" course_dir = "test_dir"
error_tracker = Mock() error_tracker = Mock()
parent_tracker = Mock()
super(DummySystem, self).__init__( super(DummySystem, self).__init__(
xmlstore=xmlstore, xmlstore=xmlstore,
course_id=course_id, course_id=course_id,
course_dir=course_dir, course_dir=course_dir,
error_tracker=error_tracker, error_tracker=error_tracker,
parent_tracker=parent_tracker,
load_error_modules=load_error_modules, load_error_modules=load_error_modules,
mixins=(InheritanceMixin, XModuleMixin), mixins=(InheritanceMixin, XModuleMixin),
field_data=KvsFieldData(DictKeyValueStore()), field_data=KvsFieldData(DictKeyValueStore()),
......
...@@ -918,7 +918,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -918,7 +918,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
# =============================== BUILTIN METHODS ========================== # =============================== BUILTIN METHODS ==========================
def __eq__(self, other): def __eq__(self, other):
return (self.scope_ids == other.scope_ids and return (hasattr(other, 'scope_ids') and
self.scope_ids == other.scope_ids and
self.fields.keys() == other.fields.keys() and self.fields.keys() == other.fields.keys() and
all(getattr(self, field.name) == getattr(other, field.name) all(getattr(self, field.name) == getattr(other, field.name)
for field in self.fields.values())) for field in self.fields.values()))
......
...@@ -163,7 +163,7 @@ class TestLTIModuleListing(ModuleStoreTestCase): ...@@ -163,7 +163,7 @@ class TestLTIModuleListing(ModuleStoreTestCase):
parent_location=self.section2.location, parent_location=self.section2.location,
display_name="lti draft", display_name="lti draft",
category="lti", category="lti",
location=self.course.id.make_usage_key('lti', 'lti_published'), location=self.course.id.make_usage_key('lti', 'lti_draft'),
publish_item=False, publish_item=False,
) )
...@@ -199,7 +199,7 @@ class TestLTIModuleListing(ModuleStoreTestCase): ...@@ -199,7 +199,7 @@ class TestLTIModuleListing(ModuleStoreTestCase):
"lti_1_1_result_service_xml_endpoint": self.expected_handler_url('grade_handler'), "lti_1_1_result_service_xml_endpoint": self.expected_handler_url('grade_handler'),
"lti_2_0_result_service_json_endpoint": "lti_2_0_result_service_json_endpoint":
self.expected_handler_url('lti_2_0_result_rest_handler') + "/user/{anon_user_id}", self.expected_handler_url('lti_2_0_result_rest_handler') + "/user/{anon_user_id}",
"display_name": self.lti_draft.display_name "display_name": self.lti_published.display_name,
} }
self.assertEqual([expected], json.loads(response.content)) self.assertEqual([expected], json.loads(response.content))
......
...@@ -1159,11 +1159,11 @@ class TestConditionalContent(TestSubmittingProblems): ...@@ -1159,11 +1159,11 @@ class TestConditionalContent(TestSubmittingProblems):
vertical_0, vertical_1 = self.split_setup(user_partition_group) vertical_0, vertical_1 = self.split_setup(user_partition_group)
# Group 0 will have 2 problems in the section, worth a total of 4 points. # Group 0 will have 2 problems in the section, worth a total of 4 points.
self.add_dropdown_to_section(vertical_0.location, 'H2P1', 1).location.html_id() self.add_dropdown_to_section(vertical_0.location, 'H2P1_GROUP0', 1).location.html_id()
self.add_dropdown_to_section(vertical_0.location, 'H2P2', 3).location.html_id() self.add_dropdown_to_section(vertical_0.location, 'H2P2_GROUP0', 3).location.html_id()
# Group 1 will have 1 problem in the section, worth a total of 1 point. # Group 1 will have 1 problem in the section, worth a total of 1 point.
self.add_dropdown_to_section(vertical_1.location, 'H2P1', 1).location.html_id() self.add_dropdown_to_section(vertical_1.location, 'H2P1_GROUP1', 1).location.html_id()
# Submit answers for problem in Section 1, which is visible to all students. # Submit answers for problem in Section 1, which is visible to all students.
self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Incorrect'}) self.submit_question_answer('H1P1', {'2_1': 'Correct', '2_2': 'Incorrect'})
...@@ -1175,8 +1175,8 @@ class TestConditionalContent(TestSubmittingProblems): ...@@ -1175,8 +1175,8 @@ class TestConditionalContent(TestSubmittingProblems):
""" """
self.split_different_problems_setup(self.user_partition_group_0) self.split_different_problems_setup(self.user_partition_group_0)
self.submit_question_answer('H2P1', {'2_1': 'Correct'}) self.submit_question_answer('H2P1_GROUP0', {'2_1': 'Correct'})
self.submit_question_answer('H2P2', {'2_1': 'Correct', '2_2': 'Incorrect', '2_3': 'Correct'}) self.submit_question_answer('H2P2_GROUP0', {'2_1': 'Correct', '2_2': 'Incorrect', '2_3': 'Correct'})
self.assertEqual(self.score_for_hw('homework1'), [1.0]) self.assertEqual(self.score_for_hw('homework1'), [1.0])
self.assertEqual(self.score_for_hw('homework2'), [1.0, 2.0]) self.assertEqual(self.score_for_hw('homework2'), [1.0, 2.0])
...@@ -1194,7 +1194,7 @@ class TestConditionalContent(TestSubmittingProblems): ...@@ -1194,7 +1194,7 @@ class TestConditionalContent(TestSubmittingProblems):
""" """
self.split_different_problems_setup(self.user_partition_group_1) self.split_different_problems_setup(self.user_partition_group_1)
self.submit_question_answer('H2P1', {'2_1': 'Correct'}) self.submit_question_answer('H2P1_GROUP1', {'2_1': 'Correct'})
self.assertEqual(self.score_for_hw('homework1'), [1.0]) self.assertEqual(self.score_for_hw('homework1'), [1.0])
self.assertEqual(self.score_for_hw('homework2'), [1.0]) self.assertEqual(self.score_for_hw('homework2'), [1.0])
...@@ -1219,7 +1219,7 @@ class TestConditionalContent(TestSubmittingProblems): ...@@ -1219,7 +1219,7 @@ class TestConditionalContent(TestSubmittingProblems):
[_, vertical_1] = self.split_setup(user_partition_group) [_, vertical_1] = self.split_setup(user_partition_group)
# Group 1 will have 1 problem in the section, worth a total of 1 point. # Group 1 will have 1 problem in the section, worth a total of 1 point.
self.add_dropdown_to_section(vertical_1.location, 'H2P1', 1).location.html_id() self.add_dropdown_to_section(vertical_1.location, 'H2P1_GROUP1', 1).location.html_id()
self.submit_question_answer('H1P1', {'2_1': 'Correct'}) self.submit_question_answer('H1P1', {'2_1': 'Correct'})
...@@ -1244,7 +1244,7 @@ class TestConditionalContent(TestSubmittingProblems): ...@@ -1244,7 +1244,7 @@ class TestConditionalContent(TestSubmittingProblems):
""" """
self.split_one_group_no_problems_setup(self.user_partition_group_1) self.split_one_group_no_problems_setup(self.user_partition_group_1)
self.submit_question_answer('H2P1', {'2_1': 'Correct'}) self.submit_question_answer('H2P1_GROUP1', {'2_1': 'Correct'})
self.assertEqual(self.score_for_hw('homework1'), [1.0]) self.assertEqual(self.score_for_hw('homework1'), [1.0])
self.assertEqual(self.score_for_hw('homework2'), [1.0]) self.assertEqual(self.score_for_hw('homework2'), [1.0])
......
...@@ -119,7 +119,8 @@ class TestFindUnit(ModuleStoreTestCase): ...@@ -119,7 +119,8 @@ class TestFindUnit(ModuleStoreTestCase):
Test finding a nested unit. Test finding a nested unit.
""" """
url = self.homework.location.to_deprecated_string() url = self.homework.location.to_deprecated_string()
self.assertEqual(tools.find_unit(self.course, url), self.homework) found_unit = tools.find_unit(self.course, url)
self.assertEqual(found_unit.location, self.homework.location)
def test_find_unit_notfound(self): def test_find_unit_notfound(self):
""" """
......
""" """
Tests of the LMS XBlock Mixin Tests of the LMS XBlock Mixin
""" """
import ddt
from django.conf import settings
from xblock.validation import ValidationMessage from xblock.validation import ValidationMessage
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.modulestore_settings import update_module_store_settings
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.partitions.partitions import Group, UserPartition from xmodule.partitions.partitions import Group, UserPartition
...@@ -13,9 +17,11 @@ class LmsXBlockMixinTestCase(ModuleStoreTestCase): ...@@ -13,9 +17,11 @@ class LmsXBlockMixinTestCase(ModuleStoreTestCase):
Base class for XBlock mixin tests cases. A simple course with a single user partition is created Base class for XBlock mixin tests cases. A simple course with a single user partition is created
in setUp for all subclasses to use. in setUp for all subclasses to use.
""" """
def build_course(self):
def setUp(self): """
super(LmsXBlockMixinTestCase, self).setUp() Build up a course tree with a UserPartition.
"""
# pylint: disable=attribute-defined-outside-init
self.user_partition = UserPartition( self.user_partition = UserPartition(
0, 0,
'first_partition', 'first_partition',
...@@ -31,13 +37,16 @@ class LmsXBlockMixinTestCase(ModuleStoreTestCase): ...@@ -31,13 +37,16 @@ class LmsXBlockMixinTestCase(ModuleStoreTestCase):
self.section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section') self.section = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
self.subsection = ItemFactory.create(parent=self.section, category='sequential', display_name='Test Subsection') self.subsection = ItemFactory.create(parent=self.section, category='sequential', display_name='Test Subsection')
self.vertical = ItemFactory.create(parent=self.subsection, category='vertical', display_name='Test Unit') self.vertical = ItemFactory.create(parent=self.subsection, category='vertical', display_name='Test Unit')
self.video = ItemFactory.create(parent=self.subsection, category='video', display_name='Test Video') self.video = ItemFactory.create(parent=self.vertical, category='video', display_name='Test Video 1')
class XBlockValidationTest(LmsXBlockMixinTestCase): class XBlockValidationTest(LmsXBlockMixinTestCase):
""" """
Unit tests for XBlock validation Unit tests for XBlock validation
""" """
def setUp(self):
super(XBlockValidationTest, self).setUp()
self.build_course()
def verify_validation_message(self, message, expected_message, expected_message_type): def verify_validation_message(self, message, expected_message, expected_message_type):
""" """
...@@ -92,6 +101,9 @@ class XBlockGroupAccessTest(LmsXBlockMixinTestCase): ...@@ -92,6 +101,9 @@ class XBlockGroupAccessTest(LmsXBlockMixinTestCase):
""" """
Unit tests for XBlock group access. Unit tests for XBlock group access.
""" """
def setUp(self):
super(XBlockGroupAccessTest, self).setUp()
self.build_course()
def test_is_visible_to_group(self): def test_is_visible_to_group(self):
""" """
...@@ -143,3 +155,90 @@ class OpenAssessmentBlockMixinTestCase(ModuleStoreTestCase): ...@@ -143,3 +155,90 @@ class OpenAssessmentBlockMixinTestCase(ModuleStoreTestCase):
Test has_score is true for ora2 problems. Test has_score is true for ora2 problems.
""" """
self.assertTrue(self.open_assessment.has_score) self.assertTrue(self.open_assessment.has_score)
@ddt.ddt
class XBlockGetParentTest(LmsXBlockMixinTestCase):
"""
Test that XBlock.get_parent returns correct results with each modulestore
backend.
"""
def _pre_setup(self):
# load the one xml course into the xml store
update_module_store_settings(
settings.MODULESTORE,
mappings={'edX/toy/2012_Fall': ModuleStoreEnum.Type.xml},
xml_store_options={
'data_dir': settings.COMMON_TEST_DATA_ROOT # where toy course lives
},
)
super(XBlockGetParentTest, self)._pre_setup()
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split, ModuleStoreEnum.Type.xml)
def test_parents(self, modulestore_type):
with self.store.default_store(modulestore_type):
# setting up our own local course tree here, since it needs to be
# created with the correct modulestore type.
if modulestore_type == 'xml':
course_key = self.store.make_course_key('edX', 'toy', '2012_Fall')
else:
course_key = self.create_toy_course('edX', 'toy', '2012_Fall_copy')
course = self.store.get_course(course_key)
self.assertIsNone(course.get_parent())
def recurse(parent):
"""
Descend the course tree and ensure the result of get_parent()
is the expected one.
"""
visited = []
for child in parent.get_children():
self.assertEqual(parent.location, child.get_parent().location)
visited.append(child)
visited += recurse(child)
return visited
visited = recurse(course)
self.assertEqual(len(visited), 28)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_parents_draft_content(self, modulestore_type):
# move the video to the new vertical
with self.store.default_store(modulestore_type):
self.build_course()
new_vertical = ItemFactory.create(parent=self.subsection, category='vertical', display_name='New Test Unit')
child_to_move_location = self.video.location.for_branch(None)
new_parent_location = new_vertical.location.for_branch(None)
old_parent_location = self.vertical.location.for_branch(None)
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
self.assertIsNone(self.course.get_parent())
with self.store.bulk_operations(self.course.id):
user_id = ModuleStoreEnum.UserID.test
old_parent = self.store.get_item(old_parent_location)
old_parent.children.remove(child_to_move_location)
self.store.update_item(old_parent, user_id)
new_parent = self.store.get_item(new_parent_location)
new_parent.children.append(child_to_move_location)
self.store.update_item(new_parent, user_id)
# re-fetch video from draft store
video = self.store.get_item(child_to_move_location)
self.assertEqual(
new_parent_location,
video.get_parent().location
)
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only):
# re-fetch video from published store
video = self.store.get_item(child_to_move_location)
self.assertEqual(
old_parent_location,
video.get_parent().location.for_branch(None)
)
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