Commit 192d7018 by Don Mitchell

Code review changes re split import/export, create_xblock

parent 21c6812b
...@@ -1400,30 +1400,30 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1400,30 +1400,30 @@ class ContentStoreTest(ContentStoreTestCase):
# crate a new module and add it as a child to a vertical # crate a new module and add it as a child to a vertical
parent = verticals[0] parent = verticals[0]
new_module = self.store.create_child( new_block = self.store.create_child(
self.user.id, parent.location, 'html', 'new_component' self.user.id, parent.location, 'html', 'new_component'
) )
# flush the cache # flush the cache
new_module = self.store.get_item(new_module.location) new_block = self.store.get_item(new_block.location)
# check for grace period definition which should be defined at the course level # check for grace period definition which should be defined at the course level
self.assertEqual(parent.graceperiod, new_module.graceperiod) self.assertEqual(parent.graceperiod, new_block.graceperiod)
self.assertEqual(parent.start, new_module.start) self.assertEqual(parent.start, new_block.start)
self.assertEqual(course.start, new_module.start) self.assertEqual(course.start, new_block.start)
self.assertEqual(course.xqa_key, new_module.xqa_key) self.assertEqual(course.xqa_key, new_block.xqa_key)
# #
# now let's define an override at the leaf node level # now let's define an override at the leaf node level
# #
new_module.graceperiod = timedelta(1) new_block.graceperiod = timedelta(1)
self.store.update_item(new_module, self.user.id) self.store.update_item(new_block, self.user.id)
# flush the cache and refetch # flush the cache and refetch
new_module = self.store.get_item(new_module.location) new_block = self.store.get_item(new_block.location)
self.assertEqual(timedelta(1), new_module.graceperiod) self.assertEqual(timedelta(1), new_block.graceperiod)
def test_default_metadata_inheritance(self): def test_default_metadata_inheritance(self):
course = CourseFactory.create() course = CourseFactory.create()
......
...@@ -163,12 +163,12 @@ class TemplateTests(unittest.TestCase): ...@@ -163,12 +163,12 @@ class TemplateTests(unittest.TestCase):
guid_locator = test_course.location.course_agnostic() guid_locator = test_course.location.course_agnostic()
# verify it can be retrieved by id # verify it can be retrieved by id
self.assertIsInstance(self.split_store.get_course(id_locator), CourseDescriptor) self.assertIsInstance(self.split_store.get_course(id_locator), CourseDescriptor)
# and by guid -- reenable when split_draft supports getting specific versions # and by guid -- TODO reenable when split_draft supports getting specific versions
# self.assertIsInstance(self.split_store.get_item(guid_locator), CourseDescriptor) # self.assertIsInstance(self.split_store.get_item(guid_locator), CourseDescriptor)
self.split_store.delete_course(id_locator, 'testbot') self.split_store.delete_course(id_locator, 'testbot')
# test can no longer retrieve by id # test can no longer retrieve by id
self.assertRaises(ItemNotFoundError, self.split_store.get_course, id_locator) self.assertRaises(ItemNotFoundError, self.split_store.get_course, id_locator)
# but can by guid # but can by guid -- same TODO as above
# self.assertIsInstance(self.split_store.get_item(guid_locator), CourseDescriptor) # self.assertIsInstance(self.split_store.get_item(guid_locator), CourseDescriptor)
def test_block_generations(self): def test_block_generations(self):
......
...@@ -603,7 +603,8 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -603,7 +603,8 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
about_location.course_key, about_location.course_key,
about_location.block_type, about_location.block_type,
block_id=about_location.block_id, block_id=about_location.block_id,
definition_data=overview_template.get('data'), definition_data={'data': overview_template.get('data')},
metadata=overview_template.get('metadata'),
runtime=runtime, runtime=runtime,
continue_version=True, continue_version=True,
) )
......
...@@ -104,7 +104,7 @@ class ModuleStoreDraftAndPublished(BranchSettingMixin): ...@@ -104,7 +104,7 @@ class ModuleStoreDraftAndPublished(BranchSettingMixin):
raise NotImplementedError raise NotImplementedError
@abstractmethod @abstractmethod
def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None): def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs):
""" """
Import the given xblock into the current branch setting: import completely overwrites any Import the given xblock into the current branch setting: import completely overwrites any
existing block of the same id. existing block of the same id.
......
...@@ -453,8 +453,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -453,8 +453,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return modulestore.create_child(user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs) return modulestore.create_child(user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs)
@strip_key @strip_key
def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None): def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs):
""" """
See :py:meth `ModuleStoreDraftAndPublished.import_xblock`
Defer to the course's modulestore if it supports this method Defer to the course's modulestore if it supports this method
""" """
store = self._verify_modulestore_support(course_key, 'import_xblock') store = self._verify_modulestore_support(course_key, 'import_xblock')
...@@ -511,6 +513,15 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -511,6 +513,15 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
def create_xblock(self, runtime, course_key, block_type, block_id=None, fields=None, **kwargs): def create_xblock(self, runtime, course_key, block_type, block_id=None, fields=None, **kwargs):
""" """
Create the new xmodule but don't save it. Returns the new module. Create the new xmodule but don't save it. Returns the new module.
Args:
runtime: :py:class `xblock.runtime` from another xblock in the same course. Providing this
significantly speeds up processing (inheritance and subsequent persistence)
course_key: :py:class `opaque_keys.CourseKey`
block_type: :py:class `string`: the string identifying the xblock type
block_id: the string uniquely identifying the block within the given course
fields: :py:class `dict` field_name, value pairs for initializing the xblock fields. Values
should be the pythonic types not the json serialized ones.
""" """
store = self._verify_modulestore_support(course_key, 'create_xblock') store = self._verify_modulestore_support(course_key, 'create_xblock')
return store.create_xblock(runtime, course_key, block_type, block_id, fields or {}, **kwargs) return store.create_xblock(runtime, course_key, block_type, block_id, fields or {}, **kwargs)
......
...@@ -974,7 +974,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -974,7 +974,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
# @Cale, should this use LocalId like we do in split? # @Cale, should this use LocalId like we do in split?
if block_id is None: if block_id is None:
block_id = uuid4().hex # really need to make this more readable: e.g., u'{}{}'.format(block_type, uuid4().hex[:4] if block_type == 'course':
block_id = course_key.run
else:
block_id = u'{}_{}'.format(block_type, uuid4().hex[:5])
if runtime is None: if runtime is None:
services = {} services = {}
...@@ -1027,7 +1030,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1027,7 +1030,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
a new identifier will be generated a new identifier will be generated
""" """
if block_id is None: if block_id is None:
block_id = uuid4().hex if block_type == 'course':
block_id = course_key.run
else:
block_id = u'{}_{}'.format(block_type, uuid4().hex[:5])
runtime = kwargs.pop('runtime', None) runtime = kwargs.pop('runtime', None)
xblock = self.create_xblock(runtime, course_key, block_type, block_id, **kwargs) xblock = self.create_xblock(runtime, course_key, block_type, block_id, **kwargs)
...@@ -1058,7 +1064,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1058,7 +1064,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return xblock return xblock
def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None): def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs):
""" """
Simple implementation of overwriting any existing xblock Simple implementation of overwriting any existing xblock
""" """
......
...@@ -534,7 +534,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -534,7 +534,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
return [ return [
BlockUsageLocator( BlockUsageLocator(
course_key=course_key, block_type=blocks[block_id]['category'], block_id=block_id course_key=course_key, block_type=blocks[block_id]['category'], block_id=block_id
).version_agnostic() )
for block_id in items for block_id in items
] ]
...@@ -818,7 +818,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -818,7 +818,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# split handles all the fields in one dict not separated by scope # split handles all the fields in one dict not separated by scope
fields = fields or {} fields = fields or {}
fields.update(kwargs.pop('metadata', {}) or {}) fields.update(kwargs.pop('metadata', {}) or {})
fields.update(kwargs.pop('definition_data', {}) or {}) definition_data = kwargs.pop('definition_data', {})
if definition_data:
if not isinstance(definition_data, dict):
definition_data = {'data': definition_data} # backward compatibility to mongo's hack
fields.update(definition_data)
# find course_index entry if applicable and structures entry # find course_index entry if applicable and structures entry
index_entry = self._get_index_if_valid(course_key, force, continue_version) index_entry = self._get_index_if_valid(course_key, force, continue_version)
...@@ -962,10 +966,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -962,10 +966,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
**kwargs **kwargs
) )
DEFAULT_ROOT_BLOCK_ID = 'course'
def create_course( def create_course(
self, org, course, run, user_id, master_branch=None, fields=None, self, org, course, run, user_id, master_branch=None, fields=None,
versions_dict=None, search_targets=None, root_category='course', versions_dict=None, search_targets=None, root_category='course',
root_block_id='course', **kwargs root_block_id=None, **kwargs
): ):
""" """
Create a new entry in the active courses index which points to an existing or new structure. Returns Create a new entry in the active courses index which points to an existing or new structure. Returns
...@@ -1043,7 +1048,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1043,7 +1048,11 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
self.db_connection.insert_definition(definition_entry) self.db_connection.insert_definition(definition_entry)
draft_structure = self._new_structure( draft_structure = self._new_structure(
user_id, root_block_id, root_category, block_fields, definition_id user_id,
root_block_id or SplitMongoModuleStore.DEFAULT_ROOT_BLOCK_ID,
root_category,
block_fields,
definition_id
) )
new_id = draft_structure['_id'] new_id = draft_structure['_id']
...@@ -1168,7 +1177,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1168,7 +1177,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
is_updated = self._compare_settings(settings, original_entry['fields']) is_updated = self._compare_settings(settings, original_entry['fields'])
# check children # check children
if partitioned_fields[Scope.children]: if partitioned_fields.get(Scope.children, {}): # purposely not 'is not None'
serialized_children = [child.block_id for child in partitioned_fields[Scope.children]['children']] serialized_children = [child.block_id for child in partitioned_fields[Scope.children]['children']]
is_updated = is_updated or original_entry['fields'].get('children', []) != serialized_children is_updated = is_updated or original_entry['fields'].get('children', []) != serialized_children
if is_updated: if is_updated:
......
...@@ -48,9 +48,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -48,9 +48,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
return item return item
def get_course(self, course_id, depth=0): def get_course(self, course_id, depth=0, **kwargs):
course_id = self._map_revision_to_branch(course_id) course_id = self._map_revision_to_branch(course_id)
return super(DraftVersioningModuleStore, self).get_course(course_id, depth=depth) return super(DraftVersioningModuleStore, self).get_course(course_id, depth=depth, **kwargs)
def get_courses(self, **kwargs): def get_courses(self, **kwargs):
""" """
...@@ -92,8 +92,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -92,8 +92,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
force=False, continue_version=False, skip_auto_publish=False, **kwargs force=False, continue_version=False, skip_auto_publish=False, **kwargs
): ):
""" """
Adds skip_auto_publish to behavior or parent. Skip_auto_publish basically just calls the super See :py:meth `ModuleStoreDraftAndPublished.create_item`
and skips all of this wrapper's functionality.
""" """
course_key = self._map_revision_to_branch(course_key) course_key = self._map_revision_to_branch(course_key)
item = super(DraftVersioningModuleStore, self).create_item( item = super(DraftVersioningModuleStore, self).create_item(
...@@ -210,9 +209,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -210,9 +209,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
location = self._map_revision_to_branch(location, revision=revision) location = self._map_revision_to_branch(location, revision=revision)
return SplitMongoModuleStore.get_parent_location(self, location, **kwargs) return SplitMongoModuleStore.get_parent_location(self, location, **kwargs)
def get_orphans(self, course_key): def get_orphans(self, course_key, **kwargs):
course_key = self._map_revision_to_branch(course_key) course_key = self._map_revision_to_branch(course_key)
return super(DraftVersioningModuleStore, self).get_orphans(course_key) return super(DraftVersioningModuleStore, self).get_orphans(course_key, **kwargs)
def has_changes(self, xblock): def has_changes(self, xblock):
""" """
...@@ -271,16 +270,25 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -271,16 +270,25 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
raise NotImplementedError() raise NotImplementedError()
def get_course_history_info(self, course_locator): def get_course_history_info(self, course_locator):
"""
See :py:meth `xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_course_history_info`
"""
course_locator = self._map_revision_to_branch(course_locator) course_locator = self._map_revision_to_branch(course_locator)
return super(DraftVersioningModuleStore, self).get_course_history_info(course_locator) return super(DraftVersioningModuleStore, self).get_course_history_info(course_locator)
def get_course_successors(self, course_locator, version_history_depth=1): def get_course_successors(self, course_locator, version_history_depth=1):
"""
See :py:meth `xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_course_successors`
"""
course_locator = self._map_revision_to_branch(course_locator) course_locator = self._map_revision_to_branch(course_locator)
return super(DraftVersioningModuleStore, self).get_course_successors( return super(DraftVersioningModuleStore, self).get_course_successors(
course_locator, version_history_depth=version_history_depth course_locator, version_history_depth=version_history_depth
) )
def get_block_generations(self, block_locator): def get_block_generations(self, block_locator):
"""
See :py:meth `xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.get_block_generations`
"""
block_locator = self._map_revision_to_branch(block_locator) block_locator = self._map_revision_to_branch(block_locator)
return super(DraftVersioningModuleStore, self).get_block_generations(block_locator) return super(DraftVersioningModuleStore, self).get_block_generations(block_locator)
...@@ -325,25 +333,25 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -325,25 +333,25 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
""" """
return block['edit_info'].get('source_version', block['edit_info']['update_version']) return block['edit_info'].get('source_version', block['edit_info']['update_version'])
def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None): def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs):
""" """
Split-based modulestores need to import published blocks to both branches Split-based modulestores need to import published blocks to both branches
""" """
# hardcode course root block id # hardcode course root block id
if block_type == 'course': if block_type == 'course':
block_id = 'course' block_id = self.DEFAULT_ROOT_BLOCK_ID
new_usage_key = course_key.make_usage_key(block_type, block_id) new_usage_key = course_key.make_usage_key(block_type, block_id)
if self.get_branch_setting(course_key) == ModuleStoreEnum.Branch.published_only: if self.get_branch_setting() == ModuleStoreEnum.Branch.published_only:
# if importing a direct only, override existing draft # if importing a direct only, override existing draft
if block_type in DIRECT_ONLY_CATEGORIES: if block_type in DIRECT_ONLY_CATEGORIES:
draft_course = course_key.for_branch(ModuleStoreEnum.BranchName.draft) draft_course = course_key.for_branch(ModuleStoreEnum.BranchName.draft)
with self.branch_setting(ModuleStoreEnum.Branch.draft_preferred, draft_course): with self.branch_setting(ModuleStoreEnum.Branch.draft_preferred, draft_course):
draft = self.import_xblock(user_id, draft_course, block_type, block_id, fields, runtime) draft = self.import_xblock(user_id, draft_course, block_type, block_id, fields, runtime)
self._auto_publish_no_children(draft.location, block_type, user_id) self._auto_publish_no_children(draft.location, block_type, user_id)
return self.get_item(new_usage_key) return self.get_item(new_usage_key.for_branch(ModuleStoreEnum.BranchName.published))
# if new to published # if new to published
elif not self.has_item(new_usage_key): elif not self.has_item(new_usage_key.for_branch(ModuleStoreEnum.BranchName.published)):
# check whether it's new to draft # check whether it's new to draft
if not self.has_item(new_usage_key.for_branch(ModuleStoreEnum.BranchName.draft)): if not self.has_item(new_usage_key.for_branch(ModuleStoreEnum.BranchName.draft)):
# add to draft too # add to draft too
......
...@@ -18,7 +18,6 @@ import random ...@@ -18,7 +18,6 @@ import random
from contextlib import contextmanager, nested from contextlib import contextmanager, nested
from shutil import rmtree from shutil import rmtree
from tempfile import mkdtemp from tempfile import mkdtemp
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.tests import CourseComparisonTest from xmodule.tests import CourseComparisonTest
......
...@@ -166,7 +166,7 @@ def import_from_xml( ...@@ -166,7 +166,7 @@ def import_from_xml(
served directly by nginx, instead of going through django. served directly by nginx, instead of going through django.
create_new_course_if_not_present: If True, then a new course is created if it doesn't already exist. create_new_course_if_not_present: If True, then a new course is created if it doesn't already exist.
Otherwise, it throws an InvalidLocationError for the course. Otherwise, it throws an InvalidLocationError if the course does not exist.
default_class, load_error_modules: are arguments for constructing the XMLModuleStore (see its doc) default_class, load_error_modules: are arguments for constructing the XMLModuleStore (see its doc)
""" """
...@@ -236,14 +236,15 @@ def import_from_xml( ...@@ -236,14 +236,15 @@ def import_from_xml(
try: try:
all_locs.remove(child.location) all_locs.remove(child.location)
except KeyError: except KeyError:
# ContentStoreTest.test_image_import has non-tree children # tolerate same child occurring under 2 parents such as in
# so, make this more robust # ContentStoreTest.test_image_import
pass pass
if verbose: if verbose:
log.debug('importing module location {loc}'.format(loc=child.location)) log.debug('importing module location {loc}'.format(loc=child.location))
_import_module_and_update_references( _import_module_and_update_references(
child, store, child,
store,
user_id, user_id,
course_key, course_key,
dest_course_id, dest_course_id,
......
...@@ -219,19 +219,22 @@ class CourseComparisonTest(unittest.TestCase): ...@@ -219,19 +219,22 @@ class CourseComparisonTest(unittest.TestCase):
self.assertEqual(len(expected_items), len(actual_items)) self.assertEqual(len(expected_items), len(actual_items))
actual_item_map = { actual_item_map = {
actual_course_key.make_usage_key(item.category, item.location.block_id): item item.location.block_id: item
for item in actual_items for item in actual_items
} }
for expected_item in expected_items: for expected_item in expected_items:
actual_item_location = actual_course_key.make_usage_key(expected_item.category, expected_item.location.block_id) actual_item_location = actual_course_key.make_usage_key(expected_item.category, expected_item.location.block_id)
# split and old mongo use different names for the course root but we don't know which
# modulestore actual's come from here; so, assume old mongo and if that fails, assume split
if expected_item.location.category == 'course': if expected_item.location.category == 'course':
actual_item_location = actual_item_location.replace(name=actual_item_location.run) actual_item_location = actual_item_location.replace(name=actual_item_location.run)
actual_item = actual_item_map.get(actual_item_location) actual_item = actual_item_map.get(actual_item_location.block_id)
# must be split
if actual_item is None and expected_item.location.category == 'course': if actual_item is None and expected_item.location.category == 'course':
actual_item_location = actual_item_location.replace(name='course') actual_item_location = actual_item_location.replace(name='course')
actual_item = actual_item_map.get(actual_item_location) actual_item = actual_item_map.get(actual_item_location.block_id)
assert actual_item is not None self.assertIsNotNone(actual_item, u'cannot find {} in {}'.format(actual_item_location, actual_item_map))
# compare fields # compare fields
self.assertEqual(expected_item.fields, actual_item.fields) self.assertEqual(expected_item.fields, actual_item.fields)
...@@ -264,14 +267,15 @@ class CourseComparisonTest(unittest.TestCase): ...@@ -264,14 +267,15 @@ class CourseComparisonTest(unittest.TestCase):
# compare children # compare children
self.assertEqual(expected_item.has_children, actual_item.has_children) self.assertEqual(expected_item.has_children, actual_item.has_children)
if expected_item.has_children: if expected_item.has_children:
actual_course_key = actual_item.location.course_key.version_agnostic()
expected_children = [ expected_children = [
course1_item_child.location.map_into_course(actual_item.location.course_key) course1_item_child.location.map_into_course(actual_course_key)
for course1_item_child in expected_item.get_children() for course1_item_child in expected_item.get_children()
# get_children was returning drafts for published parents :-( # get_children was returning drafts for published parents :-(
if expect_drafts or not getattr(course1_item_child, 'is_draft', False) if expect_drafts or not getattr(course1_item_child, 'is_draft', False)
] ]
actual_children = [ actual_children = [
item_child.location item_child.location.version_agnostic()
for item_child in actual_item.get_children() for item_child in actual_item.get_children()
# get_children was returning drafts for published parents :-( # get_children was returning drafts for published parents :-(
if expect_drafts or not getattr(item_child, 'is_draft', False) if expect_drafts or not getattr(item_child, 'is_draft', False)
......
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