Commit 483e2a6a by Calen Pennington Committed by Nimisha Asthagiri

split create_item and create_child out as separate methods

parent 06ef306f
...@@ -36,7 +36,7 @@ def get_course_updates(location, provided_id, user_id): ...@@ -36,7 +36,7 @@ def get_course_updates(location, provided_id, user_id):
try: try:
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
course_updates = modulestore().create_item(user_id, location) course_updates = modulestore().create_item(user_id, location.course_key, location.block_type, location.block_id)
course_update_items = get_course_update_items(course_updates, provided_id) course_update_items = get_course_update_items(course_updates, provided_id)
return _get_visible_update(course_update_items) return _get_visible_update(course_update_items)
...@@ -51,7 +51,7 @@ def update_course_updates(location, update, passed_id=None, user=None): ...@@ -51,7 +51,7 @@ def update_course_updates(location, update, passed_id=None, user=None):
try: try:
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
course_updates = modulestore().create_item(user.id, location) course_updates = modulestore().create_item(user.id, location.course_key, location.block_type, location.block_id)
course_update_items = list(reversed(get_course_update_items(course_updates))) course_update_items = list(reversed(get_course_update_items(course_updates)))
......
...@@ -575,7 +575,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -575,7 +575,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
location = course.id.make_usage_key('chapter', 'neuvo') location = course.id.make_usage_key('chapter', 'neuvo')
# Ensure draft mongo store does not create drafts for things that shouldn't be draft # Ensure draft mongo store does not create drafts for things that shouldn't be draft
newobject = draft_store.create_item(self.user.id, location) newobject = draft_store.create_item(self.user.id, location.course_key, location.block_type, location.block_id)
self.assertFalse(getattr(newobject, 'is_draft', False)) self.assertFalse(getattr(newobject, 'is_draft', False))
with self.assertRaises(InvalidVersionError): with self.assertRaises(InvalidVersionError):
draft_store.convert_to_draft(location, self.user.id) draft_store.convert_to_draft(location, self.user.id)
...@@ -1392,12 +1392,9 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1392,12 +1392,9 @@ class ContentStoreTest(ContentStoreTestCase):
def test_forum_id_generation(self): def test_forum_id_generation(self):
course = CourseFactory.create(org='edX', course='999', display_name='Robot Super Course') course = CourseFactory.create(org='edX', course='999', display_name='Robot Super Course')
new_component_location = course.id.make_usage_key('discussion', 'new_component')
# 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
self.store.create_item(self.user.id, new_component_location) new_discussion_item = self.store.create_item(self.user.id, course.id, 'discussion', 'new_component')
new_discussion_item = self.store.get_item(new_component_location)
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$') self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
......
...@@ -162,7 +162,7 @@ class TemplateTests(unittest.TestCase): ...@@ -162,7 +162,7 @@ class TemplateTests(unittest.TestCase):
self.assertIsInstance(self.split_store.get_course(id_locator), CourseDescriptor) self.assertIsInstance(self.split_store.get_course(id_locator), CourseDescriptor)
# and by guid # and by guid
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, ModuleStoreEnum.UserID.test) 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
...@@ -187,11 +187,11 @@ class TemplateTests(unittest.TestCase): ...@@ -187,11 +187,11 @@ class TemplateTests(unittest.TestCase):
) )
first_problem.max_attempts = 3 first_problem.max_attempts = 3
first_problem.save() # decache the above into the kvs first_problem.save() # decache the above into the kvs
updated_problem = self.split_store.update_item(first_problem, ModuleStoreEnum.UserID.test) updated_problem = self.split_store.update_item(first_problem, 'testbot')
self.assertIsNotNone(updated_problem.previous_version) self.assertIsNotNone(updated_problem.previous_version)
self.assertEqual(updated_problem.previous_version, first_problem.update_version) self.assertEqual(updated_problem.previous_version, first_problem.update_version)
self.assertNotEqual(updated_problem.update_version, first_problem.update_version) self.assertNotEqual(updated_problem.update_version, first_problem.update_version)
self.split_store.delete_item(updated_problem.location, ModuleStoreEnum.UserID.test, 'testbot') self.split_store.delete_item(updated_problem.location, 'testbot')
second_problem = persistent_factories.ItemFactory.create( second_problem = persistent_factories.ItemFactory.create(
display_name='problem 2', display_name='problem 2',
......
...@@ -34,7 +34,13 @@ class TestOrphan(CourseTestCase): ...@@ -34,7 +34,13 @@ class TestOrphan(CourseTestCase):
location = self.course.location.replace(category=category, name=name) location = self.course.location.replace(category=category, name=name)
store = modulestore() store = modulestore()
store.create_item( store.create_item(
self.user.id, location, definition_data=data, metadata=metadata, runtime=runtime self.user.id,
location.course_key,
location.block_type,
location.block_id,
definition_data=data,
metadata=metadata,
runtime=runtime
) )
if parent_name: if parent_name:
# add child to parent in mongo # add child to parent in mongo
......
...@@ -151,11 +151,11 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -151,11 +151,11 @@ class CourseTestCase(ModuleStoreTestCase):
self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft) self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft)
# create a Private (draft only) vertical # create a Private (draft only) vertical
private_vertical = self.store.create_item(self.user.id, course_id.make_usage_key('vertical', self.PRIVATE_VERTICAL)) private_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PRIVATE_VERTICAL)
self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private) self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private)
# create a Published (no draft) vertical # create a Published (no draft) vertical
public_vertical = self.store.create_item(self.user.id, course_id.make_usage_key('vertical', self.PUBLISHED_VERTICAL)) public_vertical = self.store.create_item(self.user.id, course_id, 'vertical', self.PUBLISHED_VERTICAL)
public_vertical = self.store.publish(public_vertical.location, self.user.id) public_vertical = self.store.publish(public_vertical.location, self.user.id)
self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public) self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public)
......
...@@ -287,7 +287,7 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout ...@@ -287,7 +287,7 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
if usage_key.category in CREATE_IF_NOT_FOUND: if usage_key.category in CREATE_IF_NOT_FOUND:
# New module at this location, for pages that are not pre-created. # New module at this location, for pages that are not pre-created.
# Used for course info handouts. # Used for course info handouts.
existing_item = store.create_item(user.id, usage_key) existing_item = store.create_item(user.id, usage_key.course_key, usage_key.block_type, usage_key.block_id)
else: else:
raise raise
except InvalidLocationError: except InvalidLocationError:
...@@ -416,9 +416,11 @@ def _create_item(request): ...@@ -416,9 +416,11 @@ def _create_item(request):
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
created_block = store.create_item( created_block = store.create_child(
request.user.id, request.user.id,
dest_usage_key, usage_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
definition_data=data, definition_data=data,
metadata=metadata, metadata=metadata,
runtime=parent.runtime, runtime=parent.runtime,
...@@ -437,11 +439,6 @@ def _create_item(request): ...@@ -437,11 +439,6 @@ def _create_item(request):
) )
store.update_item(course, request.user.id) store.update_item(course, request.user.id)
# TODO replace w/ nicer accessor
if not 'detached' in parent.runtime.load_block_type(category)._class_tags:
parent.children.append(created_block.location)
store.update_item(parent, request.user.id)
return JsonResponse({"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)}) return JsonResponse({"locator": unicode(created_block.location), "courseKey": unicode(created_block.location.course_key)})
...@@ -467,7 +464,9 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ ...@@ -467,7 +464,9 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
dest_module = store.create_item( dest_module = store.create_item(
user.id, user.id,
dest_usage_key, dest_usage_key.course_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
definition_data=source_item.get_explicitly_set_fields_by_scope(Scope.content), definition_data=source_item.get_explicitly_set_fields_by_scope(Scope.content),
metadata=duplicate_metadata, metadata=duplicate_metadata,
runtime=source_item.runtime, runtime=source_item.runtime,
...@@ -555,7 +554,7 @@ def _get_module_info(usage_key, user, rewrite_static_links=True): ...@@ -555,7 +554,7 @@ def _get_module_info(usage_key, user, rewrite_static_links=True):
except ItemNotFoundError: except ItemNotFoundError:
if usage_key.category in CREATE_IF_NOT_FOUND: if usage_key.category in CREATE_IF_NOT_FOUND:
# Create a new one for certain categories only. Used for course info handouts. # Create a new one for certain categories only. Used for course info handouts.
module = store.create_item(user.id, usage_key) module = store.create_item(user.id, usage_key.course_key, usage_key.block_type, block_id=usage_key.block_id)
else: else:
raise raise
......
...@@ -130,7 +130,12 @@ class CourseUpdateTest(CourseTestCase): ...@@ -130,7 +130,12 @@ class CourseUpdateTest(CourseTestCase):
''' '''
# get the updates and populate 'data' field with some data. # get the updates and populate 'data' field with some data.
location = self.course.id.make_usage_key('course_info', 'updates') location = self.course.id.make_usage_key('course_info', 'updates')
course_updates = modulestore().create_item(self.user.id, location) course_updates = modulestore().create_item(
self.user.id,
location.course_key,
location.block_type,
block_id=location.block_id
)
update_date = u"January 23, 2014" update_date = u"January 23, 2014"
update_content = u"Hello world!" update_content = u"Hello world!"
update_data = u"<ol><li><h2>" + update_date + "</h2>" + update_content + "</li></ol>" update_data = u"<ol><li><h2>" + update_date + "</h2>" + update_content + "</li></ol>"
...@@ -204,7 +209,12 @@ class CourseUpdateTest(CourseTestCase): ...@@ -204,7 +209,12 @@ class CourseUpdateTest(CourseTestCase):
'''Test trying to add to a saved course_update which is not an ol.''' '''Test trying to add to a saved course_update which is not an ol.'''
# get the updates and set to something wrong # get the updates and set to something wrong
location = self.course.id.make_usage_key('course_info', 'updates') location = self.course.id.make_usage_key('course_info', 'updates')
modulestore().create_item(self.user.id, location) modulestore().create_item(
self.user.id,
location.course_key,
location.block_type,
block_id=location.block_id
)
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
course_updates.data = 'bad news' course_updates.data = 'bad news'
modulestore().update_item(course_updates, self.user.id) modulestore().update_item(course_updates, self.user.id)
...@@ -229,8 +239,7 @@ class CourseUpdateTest(CourseTestCase): ...@@ -229,8 +239,7 @@ class CourseUpdateTest(CourseTestCase):
""" """
Test that a user can successfully post on course updates and handouts of a course Test that a user can successfully post on course updates and handouts of a course
""" """
course_key = SlashSeparatedCourseKey('Org1', 'Course_1', 'Run_1') course_update_url = self.create_update_url(course_key=self.course.id)
course_update_url = self.create_update_url(course_key=course_key)
# create a course via the view handler # create a course via the view handler
self.client.ajax_post(course_update_url) self.client.ajax_post(course_update_url)
......
...@@ -368,6 +368,25 @@ class ModuleStoreWrite(ModuleStoreRead): ...@@ -368,6 +368,25 @@ class ModuleStoreWrite(ModuleStoreRead):
pass pass
@abstractmethod @abstractmethod
def create_item(self, user_id, course_key, block_type, block_id=None, fields=None, **kwargs):
"""
Creates and saves a new item in a course.
Returns the newly created item.
Args:
user_id: ID of the user creating and saving the xmodule
course_key: A :class:`~opaque_keys.edx.CourseKey` identifying which course to create
this item in
block_type: The type of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
fields (dict): A dictionary specifying initial values for some or all fields
in the newly created block
"""
pass
@abstractmethod
def clone_course(self, source_course_id, dest_course_id, user_id): def clone_course(self, source_course_id, dest_course_id, user_id):
""" """
Sets up source_course_id to point a course with the same content as the desct_course_id. This Sets up source_course_id to point a course with the same content as the desct_course_id. This
...@@ -561,54 +580,6 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -561,54 +580,6 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
result[field.scope][field_name] = value result[field.scope][field_name] = value
return result return result
def create_item(self, user_id, location, parent_location=None, category=None, **kwargs):
"""
Creates and saves a new item.
Either location or (category, parent_location) or both must be provided.
If parent_location is provided, a new item of the given category is added as a child.
If location is not provided, a new item with the given category and given block_id
is added to the parent_location. If the block_id is not provided, a unique name
is automatically generated.
Returns the newly created item.
:param user_id: ID of the user creating and saving the xmodule
:param location: a Location--must have a category
:param parent_location: optional parameter, specifying the Location of the parent item
:param category: optional parameter for the category of the new item
:param block_id: a unique identifier for the new item
"""
raise NotImplementedError
def update_item(self, xblock, user_id, allow_not_found=False, force=False):
"""
Update the given xblock's persisted repr. Pass the user's unique id which the persistent store
should save with the update if it has that ability.
:param allow_not_found: whether this method should raise an exception if the given xblock
has not been persisted before.
:param force: fork the structure and don't update the course draftVersion if there's a version
conflict (only applicable to version tracking and conflict detecting persistence stores)
:raises VersionConflictError: if org, course, run, and version_guid given and the current
version head != version_guid and force is not True. (only applicable to version tracking stores)
"""
raise NotImplementedError
def delete_item(self, location, user_id, force=False):
"""
Delete an item from persistence. Pass the user's unique id which the persistent store
should save with the update if it has that ability.
:param user_id: ID of the user deleting the item
:param force: fork the structure and don't update the course draftVersion if there's a version
conflict (only applicable to version tracking and conflict detecting persistence stores)
:raises VersionConflictError: if org, course, run, and version_guid given and the current
version head != version_guid and force is not True. (only applicable to version tracking stores)
"""
raise NotImplementedError
def clone_course(self, source_course_id, dest_course_id, user_id): def clone_course(self, source_course_id, dest_course_id, user_id):
""" """
This base method just copies the assets. The lower level impls must do the actual cloning of This base method just copies the assets. The lower level impls must do the actual cloning of
...@@ -639,6 +610,27 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -639,6 +610,27 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
self.contentstore._drop_database() # pylint: disable=protected-access self.contentstore._drop_database() # pylint: disable=protected-access
super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs):
"""
Creates and saves a new xblock that as a child of the specified block
Returns the newly created item.
Args:
user_id: ID of the user creating and saving the xmodule
parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the
block that this item should be parented under
block_type: The type of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
fields (dict): A dictionary specifying initial values for some or all fields
in the newly created block
"""
item = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, **kwargs)
parent = self.get_item(parent_usage_key)
parent.children.append(item.location)
self.update_item(parent, user_id)
@contextmanager @contextmanager
def bulk_write_operations(self, course_id): def bulk_write_operations(self, course_id):
""" """
...@@ -699,18 +691,3 @@ class EdxJSONEncoder(json.JSONEncoder): ...@@ -699,18 +691,3 @@ class EdxJSONEncoder(json.JSONEncoder):
return obj.isoformat() return obj.isoformat()
else: else:
return super(EdxJSONEncoder, self).default(obj) return super(EdxJSONEncoder, self).default(obj)
def compute_location_from_args(location=None, parent_location=None, **kwargs):
"""
Returns a Location object that is generated from the given arguments, as follows:
If location is provided, returns it.
If location is not provided, returns a location with the given category and given block_id.
If the block_id is not provided, a unique name is automatically generated.
"""
if location is None:
block_id = kwargs.get('block_id', kwargs.get('name', kwargs.get('display_name', uuid4().hex)))
category = kwargs.pop('category')
course_key = getattr(parent_location, 'course_key', parent_location)
location = course_key.make_usage_key(category, Location.clean(block_id))
return location
...@@ -12,8 +12,8 @@ class ModuleStoreDraftAndPublished(object): ...@@ -12,8 +12,8 @@ class ModuleStoreDraftAndPublished(object):
""" """
__metaclass__ = ABCMeta __metaclass__ = ABCMeta
def __init__(self, **kwargs): def __init__(self, *args, **kwargs):
super(ModuleStoreDraftAndPublished, self).__init__(**kwargs) super(ModuleStoreDraftAndPublished, self).__init__(*args, **kwargs)
self.branch_setting_func = kwargs.pop('branch_setting_func', lambda: ModuleStoreEnum.Branch.published_only) self.branch_setting_func = kwargs.pop('branch_setting_func', lambda: ModuleStoreEnum.Branch.published_only)
@abstractmethod @abstractmethod
......
...@@ -7,21 +7,23 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule ...@@ -7,21 +7,23 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule
import logging import logging
from contextlib import contextmanager from contextlib import contextmanager
from opaque_keys import InvalidKeyError import itertools
from . import ModuleStoreWriteBase from opaque_keys import InvalidKeyError
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
import itertools
from xmodule.modulestore.split_migrator import SplitMigrator from . import ModuleStoreWriteBase
from . import ModuleStoreEnum
from .exceptions import ItemNotFoundError
from .draft_and_published import ModuleStoreDraftAndPublished
from .split_migrator import SplitMigrator
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class MixedModuleStore(ModuleStoreWriteBase): class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
""" """
ModuleStore knows how to route requests to the right persistence ms ModuleStore knows how to route requests to the right persistence ms
""" """
...@@ -282,7 +284,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -282,7 +284,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Returns: a CourseDescriptor Returns: a CourseDescriptor
""" """
store = self._get_modulestore_for_courseid(None) store = self._verify_modulestore_support(None, 'create_course')
return store.create_course(org, course, run, user_id, **kwargs) return store.create_course(org, course, run, user_id, **kwargs)
def clone_course(self, source_course_id, dest_course_id, user_id): def clone_course(self, source_course_id, dest_course_id, user_id):
...@@ -312,40 +314,57 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -312,40 +314,57 @@ class MixedModuleStore(ModuleStoreWriteBase):
source_course_id, user_id, dest_course_id.org, dest_course_id.course, dest_course_id.run source_course_id, user_id, dest_course_id.org, dest_course_id.course, dest_course_id.run
) )
def create_item(self, user_id, location=None, parent_location=None, **kwargs): def create_item(self, user_id, course_key, block_type, block_id=None, fields=None, **kwargs):
""" """
Creates and saves a new item. Creates and saves a new item in a course.
Either location or (category, parent_location) or both must be provided.
If parent_location is provided, a new item of the given category is added as a child.
If location is not provided, a new item with the given category and given block_id
is added to the parent_location. If the block_id is not provided, a unique name
is automatically generated.
Returns the newly created item. Returns the newly created item.
:param user_id: ID of the user creating and saving the xmodule Args:
:param location: a Location--must have a category user_id: ID of the user creating and saving the xmodule
:param parent_location: optional parameter, specifying the Location of the parent item course_key: A :class:`~opaque_keys.edx.CourseKey` identifying which course to create
:param category: optional parameter for the category of the new item this item in
:param block_id: a unique identifier for the new item block_type: The typo of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
fields (dict): A dictionary specifying initial values for some or all fields
in the newly created block
"""
modulestore = self._verify_modulestore_support(course_key, 'create_item')
return modulestore.create_item(user_id, course_key, block_type, block_id=block_id, fields=fields, **kwargs)
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs):
""" """
location = compute_location_from_args(location, parent_location, **kwargs) Creates and saves a new xblock that is a child of the specified block
modulestore = self._verify_modulestore_support(location, 'create_item')
return modulestore.create_item(user_id, location, parent_location, **kwargs) Returns the newly created item.
Args:
user_id: ID of the user creating and saving the xmodule
parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifying the
block that this item should be parented under
block_type: The typo of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
fields (dict): A dictionary specifying initial values for some or all fields
in the newly created block
"""
modulestore = self._verify_modulestore_support(parent_usage_key.course_key, 'create_child')
return modulestore.create_child(user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs)
def update_item(self, xblock, user_id, allow_not_found=False): def update_item(self, xblock, user_id, allow_not_found=False):
""" """
Update the xblock persisted to be the same as the given for all types of fields Update the xblock persisted to be the same as the given for all types of fields
(content, children, and metadata) attribute the change to the given user. (content, children, and metadata) attribute the change to the given user.
""" """
store = self._verify_modulestore_support(xblock.location, 'update_item') store = self._verify_modulestore_support(xblock.location.course_key, 'update_item')
return store.update_item(xblock, user_id, allow_not_found) return store.update_item(xblock, user_id, allow_not_found)
def delete_item(self, location, user_id, **kwargs): def delete_item(self, location, user_id, **kwargs):
""" """
Delete the given item from persistence. kwargs allow modulestore specific parameters. Delete the given item from persistence. kwargs allow modulestore specific parameters.
""" """
store = self._verify_modulestore_support(location, 'delete_item') store = self._verify_modulestore_support(location.course_key, 'delete_item')
store.delete_item(location, user_id=user_id, **kwargs) store.delete_item(location, user_id=user_id, **kwargs)
def revert_to_published(self, location, user_id): def revert_to_published(self, location, user_id):
...@@ -358,7 +377,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -358,7 +377,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
:raises InvalidVersionError: if no published version exists for the location specified :raises InvalidVersionError: if no published version exists for the location specified
""" """
store = self._verify_modulestore_support(location, 'revert_to_published') store = self._verify_modulestore_support(location.course_key, 'revert_to_published')
return store.revert_to_published(location, user_id) return store.revert_to_published(location, user_id)
def close_all_connections(self): def close_all_connections(self):
...@@ -388,7 +407,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -388,7 +407,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
:param runtime: if you already have an xblock from the course, the xblock.runtime value :param runtime: if you already have an xblock from the course, the xblock.runtime value
:param fields: a dictionary of field names and values for the new xmodule :param fields: a dictionary of field names and values for the new xmodule
""" """
store = self._verify_modulestore_support(location, 'create_xmodule') store = self._verify_modulestore_support(location.course_key, 'create_xmodule')
return store.create_xmodule(location, definition_data, metadata, runtime, fields, **kwargs) return store.create_xmodule(location, definition_data, metadata, runtime, fields, **kwargs)
def get_courses_for_wiki(self, wiki_slug): def get_courses_for_wiki(self, wiki_slug):
...@@ -433,7 +452,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -433,7 +452,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Save a current draft to the underlying modulestore Save a current draft to the underlying modulestore
Returns the newly published item. Returns the newly published item.
""" """
store = self._verify_modulestore_support(location, 'publish') store = self._verify_modulestore_support(location.course_key, 'publish')
return store.publish(location, user_id) return store.publish(location, user_id)
def unpublish(self, location, user_id): def unpublish(self, location, user_id):
...@@ -441,7 +460,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -441,7 +460,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Save a current draft to the underlying modulestore Save a current draft to the underlying modulestore
Returns the newly unpublished item. Returns the newly unpublished item.
""" """
store = self._verify_modulestore_support(location, 'unpublish') store = self._verify_modulestore_support(location.course_key, 'unpublish')
return store.unpublish(location, user_id) return store.unpublish(location, user_id)
def convert_to_draft(self, location, user_id): def convert_to_draft(self, location, user_id):
...@@ -451,18 +470,26 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -451,18 +470,26 @@ class MixedModuleStore(ModuleStoreWriteBase):
:param source: the location of the source (its revision must be None) :param source: the location of the source (its revision must be None)
""" """
store = self._verify_modulestore_support(location, 'convert_to_draft') store = self._verify_modulestore_support(location.course_key, 'convert_to_draft')
return store.convert_to_draft(location, user_id) return store.convert_to_draft(location, user_id)
def _verify_modulestore_support(self, location, method): def has_changes(self, usage_key):
"""
Checks if the given block has unpublished changes
:param usage_key: the block to check
:return: True if the draft and published versions differ
"""
store = self._verify_modulestore_support(usage_key.course_key, 'has_changes')
return store.has_changes(usage_key)
def _verify_modulestore_support(self, course_key, method):
""" """
Finds and returns the store that contains the course for the given location, and verifying Finds and returns the store that contains the course for the given location, and verifying
that the store supports the given method. that the store supports the given method.
Raises NotImplementedError if the found store does not support the given method. Raises NotImplementedError if the found store does not support the given method.
""" """
course_id = location.course_key store = self._get_modulestore_for_courseid(course_key)
store = self._get_modulestore_for_courseid(course_id)
if hasattr(store, method): if hasattr(store, method):
return store return store
else: else:
......
...@@ -17,6 +17,7 @@ import sys ...@@ -17,6 +17,7 @@ import sys
import logging import logging
import copy import copy
import re import re
from uuid import uuid4
from bson.son import SON from bson.son import SON
from fs.osfs import OSFS from fs.osfs import OSFS
...@@ -904,7 +905,12 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -904,7 +905,12 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
])) ]))
location = course_id.make_usage_key('course', course_id.run) location = course_id.make_usage_key('course', course_id.run)
course = self.create_item(user_id, location, fields=fields, **kwargs) course = self.create_xmodule(
location,
fields=fields,
**kwargs
)
self.update_item(course, user_id, allow_not_found=True)
# clone a default 'about' overview module as well # clone a default 'about' overview module as well
about_location = location.replace( about_location = location.replace(
...@@ -914,7 +920,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -914,7 +920,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
overview_template = AboutDescriptor.get_template('overview.yaml') overview_template = AboutDescriptor.get_template('overview.yaml')
self.create_item( self.create_item(
user_id, user_id,
about_location, about_location.course_key,
about_location.block_type,
block_id=about_location.block_id,
definition_data=overview_template.get('data'), definition_data=overview_template.get('data'),
runtime=course.system runtime=course.system
) )
...@@ -975,6 +983,52 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -975,6 +983,52 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
xmodule.save() xmodule.save()
return xmodule return xmodule
def create_item(self, user_id, course_key, block_type, block_id=None, **kwargs):
"""
Creates and saves a new item in a course.
Returns the newly created item.
Args:
user_id: ID of the user creating and saving the xmodule
course_key: A :class:`~opaque_keys.edx.CourseKey` identifying which course to create
this item in
block_type: The typo of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
"""
if block_id is None:
block_id = uuid4().hex
location = course_key.make_usage_key(block_type, block_id)
xblock = self.create_xmodule(location, **kwargs)
self.update_item(xblock, user_id, allow_not_found=True)
return xblock
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, **kwargs):
"""
Creates and saves a new xblock that as a child of the specified block
Returns the newly created item.
Args:
user_id: ID of the user creating and saving the xmodule
parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the
block that this item should be parented under
block_type: The typo of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
"""
xblock = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, **kwargs)
# attach to parent if given
if 'detached' not in xblock._class_tags:
parent = self.get_item(parent_usage_key)
parent.children.append(xblock.location)
self.update_item(parent, user_id)
return xblock
def _get_course_for_item(self, location, depth=0): def _get_course_for_item(self, location, depth=0):
''' '''
for a given Xmodule, return the course that it belongs to for a given Xmodule, return the course that it belongs to
...@@ -1065,6 +1119,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -1065,6 +1119,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
except ItemNotFoundError: except ItemNotFoundError:
if not allow_not_found: if not allow_not_found:
raise raise
elif not self.has_course(xblock.location.course_key):
raise ItemNotFoundError(xblock.location.course_key)
return xblock return xblock
......
...@@ -11,7 +11,7 @@ import logging ...@@ -11,7 +11,7 @@ import logging
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.exceptions import InvalidVersionError from xmodule.exceptions import InvalidVersionError
from xmodule.modulestore import PublishState, ModuleStoreEnum, compute_location_from_args from xmodule.modulestore import PublishState, ModuleStoreEnum
from xmodule.modulestore.exceptions import ( from xmodule.modulestore.exceptions import (
ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError
) )
...@@ -305,35 +305,6 @@ class DraftModuleStore(MongoModuleStore): ...@@ -305,35 +305,6 @@ class DraftModuleStore(MongoModuleStore):
super(DraftModuleStore, self).create_xmodule(location, definition_data, metadata, runtime, fields) super(DraftModuleStore, self).create_xmodule(location, definition_data, metadata, runtime, fields)
) )
def create_item(self, user_id, location=None, parent_location=None, **kwargs):
"""
Creates and saves a new item.
Either location or (category, parent_location) or both must be provided.
If parent_location is provided, a new item of the given category is added as a child.
If location is not provided, a new item with the given category and given block_id
is added to the parent_location. If the block_id is not provided, a unique name
is automatically generated.
Returns the newly created item.
:param user_id: ID of the user creating and saving the xmodule
:param location: a Location--must have a category
:param parent_location: optional parameter, specifying the Location of the parent item
:param category: optional parameter for the category of the new item
:param block_id: a unique identifier for the new item
"""
location = compute_location_from_args(location, parent_location, **kwargs)
xblock = self.create_xmodule(location, **kwargs)
self.update_item(xblock, user_id, allow_not_found=True)
# attach to parent if given
if parent_location is not None and not 'detached' in xblock._class_tags:
parent = self.get_item(parent_location)
parent.children.append(location)
self.update_item(parent, user_id)
return xblock
def get_items(self, course_key, settings=None, content=None, revision=None, **kwargs): def get_items(self, course_key, settings=None, content=None, revision=None, **kwargs):
""" """
Performance Note: This is generally a costly operation, but useful for wildcard searches. Performance Note: This is generally a costly operation, but useful for wildcard searches.
...@@ -511,6 +482,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -511,6 +482,7 @@ class DraftModuleStore(MongoModuleStore):
ModuleStoreEnum.RevisionOption.published_only - removes only Published versions ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
currently only provided by contentstore.views.item.orphan_handler currently only provided by contentstore.views.item.orphan_handler
Otherwise, raises a ValueError.
""" """
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred) self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
_verify_revision_is_published(location) _verify_revision_is_published(location)
...@@ -555,8 +527,10 @@ class DraftModuleStore(MongoModuleStore): ...@@ -555,8 +527,10 @@ class DraftModuleStore(MongoModuleStore):
as_functions = [as_draft, as_published] as_functions = [as_draft, as_published]
elif revision == ModuleStoreEnum.RevisionOption.published_only: elif revision == ModuleStoreEnum.RevisionOption.published_only:
as_functions = [as_published] as_functions = [as_published]
else: elif revision is None:
as_functions = [as_draft] as_functions = [as_draft]
else:
raise ValueError('revision not one of None, ModuleStoreEnum.RevisionOption.published_only, or ModuleStoreEnum.RevisionOption.all')
self._delete_subtree(location, as_functions) self._delete_subtree(location, as_functions)
def _delete_subtree(self, location, as_functions): def _delete_subtree(self, location, as_functions):
......
...@@ -85,8 +85,8 @@ class SplitMigrator(object): ...@@ -85,8 +85,8 @@ class SplitMigrator(object):
# the 'children' field as it goes. # the 'children' field as it goes.
_new_module = self.split_modulestore.create_item( _new_module = self.split_modulestore.create_item(
user_id, user_id,
course_version_locator.make_usage_key(module.location.category, module.location.block_id), course_version_locator,
parent_location=course_version_locator, module.location.block_type,
block_id=module.location.block_id, block_id=module.location.block_id,
fields=self._get_json_fields_translate_references( fields=self._get_json_fields_translate_references(
module, course_version_locator, new_course.location.block_id module, course_version_locator, new_course.location.block_id
...@@ -131,7 +131,8 @@ class SplitMigrator(object): ...@@ -131,7 +131,8 @@ class SplitMigrator(object):
else: else:
# only a draft version (aka, 'private'). # only a draft version (aka, 'private').
_new_module = self.split_modulestore.create_item( _new_module = self.split_modulestore.create_item(
user_id, new_locator, parent_location=new_draft_course_loc, user_id, new_draft_course_loc,
new_locator.block_type,
block_id=new_locator.block_id, block_id=new_locator.block_id,
fields=self._get_json_fields_translate_references( fields=self._get_json_fields_translate_references(
module, new_draft_course_loc, published_course_usage_key.block_id module, new_draft_course_loc, published_course_usage_key.block_id
......
...@@ -65,7 +65,7 @@ from opaque_keys.edx.locator import ( ...@@ -65,7 +65,7 @@ from opaque_keys.edx.locator import (
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError, \ from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError, \
DuplicateCourseError DuplicateCourseError
from xmodule.modulestore import ( from xmodule.modulestore import (
inheritance, ModuleStoreWriteBase, ModuleStoreEnum, compute_location_from_args inheritance, ModuleStoreWriteBase, ModuleStoreEnum
) )
from ..exceptions import ItemNotFoundError from ..exceptions import ItemNotFoundError
...@@ -267,7 +267,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -267,7 +267,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
:param course_version_guid: if provided, clear only this entry :param course_version_guid: if provided, clear only this entry
""" """
if course_version_guid: if course_version_guid:
del self.thread_cache.course_cache[course_version_guid] try:
del self.thread_cache.course_cache[course_version_guid]
except KeyError:
pass
else: else:
self.thread_cache.course_cache = {} self.thread_cache.course_cache = {}
...@@ -630,7 +633,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -630,7 +633,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Find the history of this block. Return as a VersionTree of each place the block changed (except Find the history of this block. Return as a VersionTree of each place the block changed (except
deletion). deletion).
The block's history tracks its explicit changes but not the changes in its children. The block's history tracks its explicit changes but not the changes in its children starting
from when the block was created.
''' '''
# course_agnostic means we don't care if the head and version don't align, trust the version # course_agnostic means we don't care if the head and version don't align, trust the version
...@@ -640,7 +644,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -640,7 +644,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
all_versions_with_block = self.db_connection.find_matching_structures( all_versions_with_block = self.db_connection.find_matching_structures(
{ {
'original_version': course_struct['original_version'], 'original_version': course_struct['original_version'],
update_version_field: {'$exists': True} update_version_field: {'$exists': True},
} }
) )
# find (all) root versions and build map {previous: {successors}..} # find (all) root versions and build map {previous: {successors}..}
...@@ -650,6 +654,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -650,6 +654,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
block_payload = self._get_block_from_structure(version, block_id) block_payload = self._get_block_from_structure(version, block_id)
if version['_id'] == block_payload['edit_info']['update_version']: if version['_id'] == block_payload['edit_info']['update_version']:
if block_payload['edit_info'].get('previous_version') is None: if block_payload['edit_info'].get('previous_version') is None:
# this was when this block was created
possible_roots.append(block_payload['edit_info']['update_version']) possible_roots.append(block_payload['edit_info']['update_version'])
else: # map previous to {update..} else: # map previous to {update..}
result.setdefault(block_payload['edit_info']['previous_version'], set()).add( result.setdefault(block_payload['edit_info']['previous_version'], set()).add(
...@@ -763,19 +768,16 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -763,19 +768,16 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
serial += 1 serial += 1
return category + str(serial) return category + str(serial)
# DHM: Should I rewrite this to take a new xblock instance rather than to construct it? That is, require the def create_item(
# caller to use XModuleDescriptor.load_from_json thus reducing similar code and making the object creation and self, user_id, course_key, block_type, block_id=None,
# validation behavior a responsibility of the model layer rather than the persistence layer. definition_locator=None, fields=None,
def create_item(self, user_id, location=None, parent_location=None, force=False, continue_version=False, **kwargs
definition_locator=None, force=False, continue_version=False, **kwargs
): ):
""" """
Add a descriptor to persistence as the last child of the optional parent_location or just as an element Add a descriptor to persistence as an element
of the course (if no parent provided). Return the resulting post saved version with populated locators. of the course. Return the resulting post saved version with populated locators.
:param course_or_parent_locator: If BlockUsageLocator, then it's assumed to be the parent. :param course_key: If it has a version_guid and a course org + course + run + branch, this
If it's a CourseLocator, then it's
merely the containing course. If it has a version_guid and a course org + course + run + branch, this
method ensures that the version is the head of the given course branch before making the change. method ensures that the version is the head of the given course branch before making the change.
raises InsufficientSpecificationError if there is no course locator. raises InsufficientSpecificationError if there is no course locator.
...@@ -815,27 +817,15 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -815,27 +817,15 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
the course id'd by version_guid but instead in one w/ a new version_guid. Ensure in this case that you get the course id'd by version_guid but instead in one w/ a new version_guid. Ensure in this case that you get
the new version_guid from the locator in the returned object! the new version_guid from the locator in the returned object!
""" """
location = compute_location_from_args(location, parent_location, **kwargs)
if not isinstance(location, (CourseLocator, BlockUsageLocator)):
raise ValueError(u"Cannot create item {} in split. Wrong repr.".format(location))
# convert fields into a single dict if separated by scope
fields = kwargs.get('fields', {})
fields.update(kwargs.pop('metadata', {}))
fields.update(kwargs.pop('definition_data', {}))
kwargs['fields'] = fields
# 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(location, force, continue_version) index_entry = self._get_index_if_valid(course_key, force, continue_version)
structure = self._lookup_course(location)['structure'] structure = self._lookup_course(course_key)['structure']
category = location.block_type partitioned_fields = self.partition_fields_by_scope(block_type, fields)
partitioned_fields = self.partition_fields_by_scope(category, fields)
new_def_data = partitioned_fields.get(Scope.content, {}) new_def_data = partitioned_fields.get(Scope.content, {})
# persist the definition if persisted != passed # persist the definition if persisted != passed
if (definition_locator is None or isinstance(definition_locator.definition_id, LocalId)): if (definition_locator is None or isinstance(definition_locator.definition_id, LocalId)):
definition_locator = self.create_definition_from_data(new_def_data, category, user_id) definition_locator = self.create_definition_from_data(new_def_data, block_type, user_id)
elif new_def_data is not None: elif new_def_data is not None:
definition_locator, _ = self.update_definition_from_data(definition_locator, new_def_data, user_id) definition_locator, _ = self.update_definition_from_data(definition_locator, new_def_data, user_id)
...@@ -848,22 +838,21 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -848,22 +838,21 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
new_id = new_structure['_id'] new_id = new_structure['_id']
# generate usage id # generate usage id
block_id = kwargs.pop('block_id', location.block_id)
if block_id is not None: if block_id is not None:
if encode_key_for_mongo(block_id) in new_structure['blocks']: if encode_key_for_mongo(block_id) in new_structure['blocks']:
raise DuplicateItemError(block_id, self, 'structures') raise DuplicateItemError(block_id, self, 'structures')
else: else:
new_block_id = block_id new_block_id = block_id
else: else:
new_block_id = self._generate_block_id(new_structure['blocks'], category) new_block_id = self._generate_block_id(new_structure['blocks'], block_type)
block_fields = partitioned_fields.get(Scope.settings, {}) block_fields = partitioned_fields.get(Scope.settings, {})
if Scope.children in partitioned_fields: if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children]) block_fields.update(partitioned_fields[Scope.children])
self._update_block_in_structure(new_structure, new_block_id, { self._update_block_in_structure(new_structure, new_block_id, {
"category": category, "category": block_type,
"definition": definition_locator.definition_id, "definition": definition_locator.definition_id,
"fields": self._serialize_fields(category, block_fields), "fields": self._serialize_fields(block_type, block_fields),
'edit_info': { 'edit_info': {
'edited_on': datetime.datetime.now(UTC), 'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id, 'edited_by': user_id,
...@@ -872,17 +861,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -872,17 +861,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
} }
}) })
# if given parent, add new block as child and update parent's version
parent = None
if isinstance(parent_location, BlockUsageLocator) and parent_location.block_id is not None:
encoded_block_id = encode_key_for_mongo(parent_location.block_id)
parent = new_structure['blocks'][encoded_block_id]
parent['fields'].setdefault('children', []).append(new_block_id)
if not continue_version or parent['edit_info']['update_version'] != structure['_id']:
parent['edit_info']['edited_on'] = datetime.datetime.now(UTC)
parent['edit_info']['edited_by'] = user_id
parent['edit_info']['previous_version'] = parent['edit_info']['update_version']
parent['edit_info']['update_version'] = new_id
if continue_version: if continue_version:
# db update # db update
self.db_connection.update_structure(new_structure) self.db_connection.update_structure(new_structure)
...@@ -894,22 +872,68 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -894,22 +872,68 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# update the index entry if appropriate # update the index entry if appropriate
if index_entry is not None: if index_entry is not None:
if not continue_version: if not continue_version:
self._update_head(index_entry, location.branch, new_id) self._update_head(index_entry, course_key.branch, new_id)
item_loc = BlockUsageLocator( item_loc = BlockUsageLocator(
location.version_agnostic(), course_key.version_agnostic(),
block_type=category, block_type=block_type,
block_id=new_block_id, block_id=new_block_id,
) )
else: else:
item_loc = BlockUsageLocator( item_loc = BlockUsageLocator(
CourseLocator(version_guid=new_id), CourseLocator(version_guid=new_id),
block_type=category, block_type=block_type,
block_id=new_block_id, block_id=new_block_id,
) )
# reconstruct the new_item from the cache # reconstruct the new_item from the cache
return self.get_item(item_loc) return self.get_item(item_loc)
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, continue_version=False, **kwargs):
"""
Creates and saves a new xblock that as a child of the specified block
Returns the newly created item.
Args:
user_id: ID of the user creating and saving the xmodule
parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifying the
block that this item should be parented under
block_type: The typo of block to create
block_id: a unique identifier for the new item. If not supplied,
a new identifier will be generated
fields (dict): A dictionary specifying initial values for some or all fields
in the newly created block
"""
xblock = self.create_item(
user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields,
continue_version=continue_version,
**kwargs)
# don't version the structure as create_item handled that already.
new_structure = self._lookup_course(xblock.location.course_key)['structure']
# add new block as child and update parent's version
encoded_block_id = encode_key_for_mongo(parent_usage_key.block_id)
parent = new_structure['blocks'][encoded_block_id]
parent['fields'].setdefault('children', []).append(xblock.location.block_id)
if parent['edit_info']['update_version'] != new_structure['_id']:
# if the parent hadn't been previously changed in this bulk transaction, indicate that it's
# part of the bulk transaction
parent['edit_info'] = {
'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id,
'previous_version': parent['edit_info']['update_version'],
'update_version': new_structure['_id'],
}
# db update
self.db_connection.update_structure(new_structure)
# clear cache so things get refetched and inheritance recomputed
self._clear_cache(new_structure['_id'])
# don't need to update the index b/c create_item did it for this version
return xblock
def clone_course(self, source_course_id, dest_course_id, user_id): def clone_course(self, source_course_id, dest_course_id, user_id):
""" """
See :meth: `.ModuleStoreWrite.clone_course` for documentation. See :meth: `.ModuleStoreWrite.clone_course` for documentation.
......
...@@ -6,6 +6,7 @@ from ..exceptions import ItemNotFoundError ...@@ -6,6 +6,7 @@ from ..exceptions import ItemNotFoundError
from split import SplitMongoModuleStore from split import SplitMongoModuleStore
from xmodule.modulestore import ModuleStoreEnum, PublishState from xmodule.modulestore import ModuleStoreEnum, PublishState
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore): class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore):
...@@ -13,13 +14,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -13,13 +14,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
A subclass of Split that supports a dual-branch fall-back versioning framework A subclass of Split that supports a dual-branch fall-back versioning framework
with a Draft branch that falls back to a Published branch. with a Draft branch that falls back to a Published branch.
""" """
def __init__(self, **kwargs):
super(DraftVersioningModuleStore, self).__init__(**kwargs)
def create_course(self, org, course, run, user_id, **kwargs): def create_course(self, org, course, run, user_id, **kwargs):
master_branch = kwargs.pop('master_branch', ModuleStoreEnum.BranchName.draft) master_branch = kwargs.pop('master_branch', ModuleStoreEnum.BranchName.draft)
return super(DraftVersioningModuleStore, self).create_course( return super(DraftVersioningModuleStore, self).create_course(
org, course, run, user_id, master_branch, **kwargs org, course, run, user_id, master_branch=master_branch, **kwargs
) )
def get_courses(self): def get_courses(self):
...@@ -31,32 +29,81 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -31,32 +29,81 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
def delete_item(self, location, user_id, revision=None, **kwargs): def delete_item(self, location, user_id, revision=None, **kwargs):
""" """
Delete the given item from persistence. kwargs allow modulestore specific parameters. Delete the given item from persistence. kwargs allow modulestore specific parameters.
Args:
location: UsageKey of the item to be deleted
user_id: id of the user deleting the item
revision:
None - deletes the item and its subtree, and updates the parents per description above
ModuleStoreEnum.RevisionOption.published_only - removes only Published versions
ModuleStoreEnum.RevisionOption.all - removes both Draft and Published parents
currently only provided by contentstore.views.item.orphan_handler
Otherwise, raises a ValueError.
""" """
if revision == ModuleStoreEnum.RevisionOption.published_only: if revision == ModuleStoreEnum.RevisionOption.published_only:
branches_to_delete = [ModuleStoreEnum.BranchName.published] branches_to_delete = [ModuleStoreEnum.BranchName.published]
elif revision == ModuleStoreEnum.RevisionOption.all: elif revision == ModuleStoreEnum.RevisionOption.all:
branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft] branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
else: elif revision is None:
branches_to_delete = [ModuleStoreEnum.BranchName.draft] branches_to_delete = [ModuleStoreEnum.BranchName.draft]
else:
raise ValueError('revision not one of None, ModuleStoreEnum.RevisionOption.published_only, or ModuleStoreEnum.RevisionOption.all')
for branch in branches_to_delete: for branch in branches_to_delete:
SplitMongoModuleStore.delete_item(self, location.for_branch(branch), user_id, **kwargs) SplitMongoModuleStore.delete_item(self, location.for_branch(branch), user_id, **kwargs)
def get_item(self, usage_key, depth=0, revision=None): def _map_revision_to_branch(self, key, revision=None):
""" """
Returns the item identified by usage_key and revision. Maps RevisionOptions to BranchNames, inserting them into the key
""" """
def for_branch(branch_state):
if usage_key.branch is not None and usage_key.branch is not branch_state:
raise ValueError('{} already has a branch.'.format(usage_key))
return usage_key.for_branch(branch_state)
if revision == ModuleStoreEnum.RevisionOption.published_only: if revision == ModuleStoreEnum.RevisionOption.published_only:
usage_key = for_branch(ModuleStoreEnum.BranchName.published) return key.for_branch(ModuleStoreEnum.BranchName.published)
elif revision == ModuleStoreEnum.RevisionOption.draft_only: elif revision == ModuleStoreEnum.RevisionOption.draft_only:
usage_key = for_branch(ModuleStoreEnum.BranchName.draft) return key.for_branch(ModuleStoreEnum.BranchName.draft)
else:
return key
def has_item(self, usage_key, revision=None):
"""
Returns True if location exists in this ModuleStore.
"""
usage_key = self._map_revision_to_branch(usage_key, revision=revision)
return super(DraftVersioningModuleStore, self).has_item(usage_key)
def get_item(self, usage_key, depth=0, revision=None):
"""
Returns the item identified by usage_key and revision.
"""
usage_key = self._map_revision_to_branch(usage_key, revision=revision)
return super(DraftVersioningModuleStore, self).get_item(usage_key, depth=depth) return super(DraftVersioningModuleStore, self).get_item(usage_key, depth=depth)
def get_items(self, course_locator, settings=None, content=None, revision=None, **kwargs):
"""
Returns a list of XModuleDescriptor instances for the matching items within the course with
the given course_locator.
"""
course_locator = self._map_revision_to_branch(course_locator, revision=revision)
return super(DraftVersioningModuleStore, self).get_items(
course_locator,
settings=settings,
content=content,
**kwargs
)
def get_parent_location(self, location, revision=None, **kwargs): def get_parent_location(self, location, revision=None, **kwargs):
# NAATODO - support draft_preferred '''
Returns the given location's parent location in this course.
Args:
revision:
None - uses the branch setting for the revision
ModuleStoreEnum.RevisionOption.published_only
- return only the PUBLISHED parent if it exists, else returns None
ModuleStoreEnum.RevisionOption.draft_preferred
- return either the DRAFT or PUBLISHED parent, preferring DRAFT, if parent(s) exists,
else returns None
'''
if revision == ModuleStoreEnum.RevisionOption.draft_preferred:
revision = ModuleStoreEnum.RevisionOption.draft_only
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 has_changes(self, usage_key): def has_changes(self, usage_key):
...@@ -65,6 +112,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -65,6 +112,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
:param usage_key: the block to check :param usage_key: the block to check
:return: True if the draft and published versions differ :return: True if the draft and published versions differ
""" """
# TODO for better performance: lookup the courses and get the block entry, don't create the instances
draft = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.draft)) draft = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.draft))
try: try:
published = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.published)) published = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.published))
...@@ -120,20 +168,21 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -120,20 +168,21 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch))['structure'] course_structure = self._lookup_course(xblock.location.course_key.for_branch(branch))['structure']
return self._get_block_from_structure(course_structure, xblock.location.block_id) return self._get_block_from_structure(course_structure, xblock.location.block_id)
if xblock.location.branch is None:
raise ValueError(u'{} is not in a branch; so, this is nonsensical'.format(xblock.location))
if xblock.location.branch == ModuleStoreEnum.BranchName.draft: if xblock.location.branch == ModuleStoreEnum.BranchName.draft:
other = get_head(ModuleStoreEnum.BranchName.published) try:
other = get_head(ModuleStoreEnum.BranchName.published)
except ItemNotFoundError:
return PublishState.private
elif xblock.location.branch == ModuleStoreEnum.BranchName.published: elif xblock.location.branch == ModuleStoreEnum.BranchName.published:
other = get_head(ModuleStoreEnum.BranchName.draft) other = get_head(ModuleStoreEnum.BranchName.draft)
else: else:
raise ValueError(u'{} is not in a branch other than draft or published; so, this is nonsensical'.format(xblock.location)) raise ValueError(u'{} is in a branch other than draft or published.'.format(xblock.location))
if not other: if not other:
if xblock.location.branch == ModuleStoreEnum.BranchName.draft: if xblock.location.branch == ModuleStoreEnum.BranchName.draft:
return PublishState.private return PublishState.private
else: else:
return PublishState.public # a bit nonsensical return PublishState.public
elif xblock.update_version != other['edit_info']['update_version']: elif xblock.update_version != other['edit_info']['update_version']:
return PublishState.draft return PublishState.draft
else: else:
......
...@@ -174,7 +174,15 @@ class ItemFactory(XModuleFactory): ...@@ -174,7 +174,15 @@ class ItemFactory(XModuleFactory):
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 runtime = parent.runtime if parent else None
store.create_item(user_id, location, metadata=metadata, definition_data=data, runtime=runtime) store.create_item(
user_id,
location.course_key,
location.block_type,
block_id=location.block_id,
metadata=metadata,
definition_data=data,
runtime=runtime
)
module = store.get_item(location) module = store.get_item(location)
......
...@@ -4,6 +4,7 @@ from xmodule.course_module import CourseDescriptor ...@@ -4,6 +4,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
import factory import factory
from factory.helpers import lazy_attribute from factory.helpers import lazy_attribute
from opaque_keys.edx.keys import UsageKey
# Factories don't have __init__ methods, and are self documenting # Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232, C0111 # pylint: disable=W0232, C0111
...@@ -73,10 +74,16 @@ class ItemFactory(SplitFactory): ...@@ -73,10 +74,16 @@ class ItemFactory(SplitFactory):
:param definition_locator (optional): the DescriptorLocator for the definition this uses or branches :param definition_locator (optional): the DescriptorLocator for the definition this uses or branches
""" """
modulestore = kwargs.pop('modulestore') modulestore = kwargs.pop('modulestore')
return modulestore.create_item( if isinstance(parent_location, UsageKey):
user_id, category=category, parent_location=parent_location, defintion_locator=definition_locator, return modulestore.create_child(
force=force, continue_version=continue_version, **kwargs user_id, parent_location, category, defintion_locator=definition_locator,
) force=force, continue_version=continue_version, **kwargs
)
else:
return modulestore.create_item(
user_id, parent_location, category, defintion_locator=definition_locator,
force=force, continue_version=continue_version, **kwargs
)
@classmethod @classmethod
def _build(cls, target_class, *args, **kwargs): def _build(cls, target_class, *args, **kwargs):
......
...@@ -117,24 +117,21 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -117,24 +117,21 @@ class TestMixedModuleStore(unittest.TestCase):
""" """
Create a course w/ one item in the persistence store using the given course & item location. Create a course w/ one item in the persistence store using the given course & item location.
""" """
# create course
self.course = self.store.create_course(course_key.org, course_key.course, course_key.run, self.user_id) self.course = self.store.create_course(course_key.org, course_key.course, course_key.run, self.user_id)
self.writable_chapter_location = self.course.id.make_usage_key('chapter', 'Overview').version_agnostic()
block_id = self.writable_chapter_location.name
chapter = self.store.create_item(
# don't use course_location as it may not be the repr
self.user_id, self.writable_chapter_location,
parent_location=self.course.location, block_id=block_id
)
if isinstance(self.course.id, CourseLocator): if isinstance(self.course.id, CourseLocator):
self.course_locations[self.MONGO_COURSEID] = self.course.location.version_agnostic() self.course_locations[self.MONGO_COURSEID] = self.course.location.version_agnostic()
else: else:
self.assertEqual(self.course.id, course_key) self.assertEqual(self.course.id, course_key)
self.assertEqual(chapter.location, self.writable_chapter_location)
# create chapter
chapter = self.store.create_child(self.user_id, self.course.location, 'chapter', block_id='Overview')
self.writable_chapter_location = chapter.location.version_agnostic()
def _create_block_hierarchy(self): def _create_block_hierarchy(self):
""" """
Creates a hierarchy of blocks for testing Creates a hierarchy of blocks for testing
Each block is assigned as a field of the class and can be easily accessed Each block's (version_agnostic) location is assigned as a field of the class and can be easily accessed
""" """
BlockInfo = namedtuple('BlockInfo', 'field_name, category, display_name, sub_tree') BlockInfo = namedtuple('BlockInfo', 'field_name, category, display_name, sub_tree')
...@@ -173,15 +170,13 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -173,15 +170,13 @@ class TestMixedModuleStore(unittest.TestCase):
] ]
def create_sub_tree(parent, block_info): def create_sub_tree(parent, block_info):
block = self.store.create_item( block = self.store.create_child(
self.user_id, parent_location=parent.location.version_agnostic(), self.user_id, parent.location.version_agnostic(),
category=block_info.category, block_id=block_info.display_name block_info.category, block_id=block_info.display_name
) )
for tree in block_info.sub_tree: for tree in block_info.sub_tree:
create_sub_tree(block, tree) create_sub_tree(block, tree)
# reload the block to update its children field setattr(self, block_info.field_name, block.location.version_agnostic())
block = self.store.get_item(block.location.version_agnostic())
setattr(self, block_info.field_name, block)
for tree in trees: for tree in trees:
create_sub_tree(self.course, tree) create_sub_tree(self.course, tree)
...@@ -317,17 +312,17 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -317,17 +312,17 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.delete_item(self.writable_chapter_location, self.user_id) self.store.delete_item(self.writable_chapter_location, self.user_id)
# verify it's gone # verify it's gone
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_item(self.writable_chapter_location.version_agnostic()) self.store.get_item(self.writable_chapter_location)
# create and delete a private vertical with private children # create and delete a private vertical with private children
private_vert = self.store.create_item( private_vert = self.store.create_child(
# don't use course_location as it may not be the repr # don't use course_location as it may not be the repr
self.user_id, parent_location=self.course_locations[self.MONGO_COURSEID], self.user_id, self.course_locations[self.MONGO_COURSEID],
category='vertical', block_id='private' 'vertical', block_id='private'
) )
private_leaf = self.store.create_item( private_leaf = self.store.create_child(
# don't use course_location as it may not be the repr # don't use course_location as it may not be the repr
self.user_id, parent_location=private_vert.location, category='html', block_id='private_leaf' self.user_id, private_vert.location, 'html', block_id='private_leaf'
) )
# verify pre delete state (just to verify that the test is valid) # verify pre delete state (just to verify that the test is valid)
...@@ -357,18 +352,18 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -357,18 +352,18 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertFalse(self.store.has_item(leaf_loc)) self.assertFalse(self.store.has_item(leaf_loc))
self.assertNotIn(vert_loc, course.children) self.assertNotIn(vert_loc, course.children)
# NAATODO enable for split after your converge merge # TODO can remove this once LMS-2869 is implemented
if default_ms == 'split': # first create a Published branch
return self.store.publish(self.course_locations[self.MONGO_COURSEID], self.user_id)
# reproduce bug STUD-1965 # reproduce bug STUD-1965
# create and delete a private vertical with private children # create and delete a private vertical with private children
private_vert = self.store.create_item( private_vert = self.store.create_child(
# don't use course_location as it may not be the repr # don't use course_location as it may not be the repr
self.course_locations[self.MONGO_COURSEID], 'vertical', user_id=self.user_id, block_id='publish' self.user_id, self.course_locations[self.MONGO_COURSEID], 'vertical', block_id='publish'
) )
private_leaf = self.store.create_item( private_leaf = self.store.create_child(
private_vert.location, 'html', user_id=self.user_id, block_id='bug_leaf' self.user_id, private_vert.location, 'html', block_id='bug_leaf'
) )
self.store.publish(private_vert.location, self.user_id) self.store.publish(private_vert.location, self.user_id)
...@@ -435,14 +430,13 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -435,14 +430,13 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(parent, self.course_locations[self.XML_COURSEID1]) self.assertEqual(parent, self.course_locations[self.XML_COURSEID1])
def verify_get_parent_locations_results(self, expected_results): def verify_get_parent_locations_results(self, expected_results):
# expected_results should be a list of (child, parent, revision) for child_location, parent_location, revision in expected_results:
for test in expected_results:
self.assertEqual( self.assertEqual(
test[1].location if test[1] else None, parent_location,
self.store.get_parent_location(test[0].location, revision=test[2]) self.store.get_parent_location(child_location, revision=revision)
) )
@ddt.data('draft') @ddt.data('draft', 'split')
def test_get_parent_locations_moved_child(self, default_ms): def test_get_parent_locations_moved_child(self, default_ms):
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
...@@ -451,30 +445,34 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -451,30 +445,34 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
# make drafts of verticals # make drafts of verticals
self.store.convert_to_draft(self.vertical_x1a.location, self.user_id) self.store.convert_to_draft(self.vertical_x1a, self.user_id)
self.store.convert_to_draft(self.vertical_y1a.location, self.user_id) self.store.convert_to_draft(self.vertical_y1a, self.user_id)
# move child problem_x1a_1 to vertical_y1a # move child problem_x1a_1 to vertical_y1a
child_to_move = self.problem_x1a_1 child_to_move_location = self.problem_x1a_1
old_parent = self.vertical_x1a new_parent_location = self.vertical_y1a
new_parent = self.vertical_y1a old_parent_location = self.vertical_x1a
old_parent.children.remove(child_to_move.location)
new_parent.children.append(child_to_move.location) old_parent = self.store.get_item(old_parent_location)
old_parent.children.remove(child_to_move_location.replace(version_guid=old_parent.location.version_guid))
self.store.update_item(old_parent, self.user_id) self.store.update_item(old_parent, self.user_id)
new_parent = self.store.get_item(new_parent_location)
new_parent.children.append(child_to_move_location.replace(version_guid=new_parent.location.version_guid))
self.store.update_item(new_parent, self.user_id) self.store.update_item(new_parent, self.user_id)
self.verify_get_parent_locations_results([ self.verify_get_parent_locations_results([
(child_to_move, new_parent, None), (child_to_move_location, new_parent_location, None),
(child_to_move, new_parent, ModuleStoreEnum.RevisionOption.draft_preferred), (child_to_move_location, new_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_move, old_parent, ModuleStoreEnum.RevisionOption.published_only), (child_to_move_location, old_parent_location.for_branch(ModuleStoreEnum.BranchName.published), ModuleStoreEnum.RevisionOption.published_only),
]) ])
# publish the course again # publish the course again
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
self.verify_get_parent_locations_results([ self.verify_get_parent_locations_results([
(child_to_move, new_parent, None), (child_to_move_location, new_parent_location, None),
(child_to_move, new_parent, ModuleStoreEnum.RevisionOption.draft_preferred), (child_to_move_location, new_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_move, new_parent, ModuleStoreEnum.RevisionOption.published_only), (child_to_move_location, new_parent_location.for_branch(ModuleStoreEnum.BranchName.published), ModuleStoreEnum.RevisionOption.published_only),
]) ])
@ddt.data('draft') @ddt.data('draft')
...@@ -486,29 +484,28 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -486,29 +484,28 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
# make draft of vertical # make draft of vertical
self.store.convert_to_draft(self.vertical_y1a.location, self.user_id) self.store.convert_to_draft(self.vertical_y1a, self.user_id)
# delete child problem_y1a_1 # delete child problem_y1a_1
child_to_delete = self.problem_y1a_1 child_to_delete_location = self.problem_y1a_1
old_parent = self.vertical_y1a old_parent_location = self.vertical_y1a
self.store.delete_item(child_to_delete.location, self.user_id) self.store.delete_item(child_to_delete_location, self.user_id)
self.verify_get_parent_locations_results([ self.verify_get_parent_locations_results([
(child_to_delete, old_parent, None), (child_to_delete_location, old_parent_location, None),
# Note: The following could be an unexpected result, but we want to avoid an extra database call # Note: The following could be an unexpected result, but we want to avoid an extra database call
(child_to_delete, old_parent, ModuleStoreEnum.RevisionOption.draft_preferred), (child_to_delete_location, old_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_delete, old_parent, ModuleStoreEnum.RevisionOption.published_only), (child_to_delete_location, old_parent_location, ModuleStoreEnum.RevisionOption.published_only),
]) ])
# publish the course again # publish the course again
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
self.verify_get_parent_locations_results([ self.verify_get_parent_locations_results([
(child_to_delete, None, None), (child_to_delete_location, None, None),
(child_to_delete, None, ModuleStoreEnum.RevisionOption.draft_preferred), (child_to_delete_location, None, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_delete, None, ModuleStoreEnum.RevisionOption.published_only), (child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only),
]) ])
@ddt.data('draft') @ddt.data('draft')
def test_revert_to_published_root_draft(self, default_ms): def test_revert_to_published_root_draft(self, default_ms):
""" """
...@@ -516,23 +513,25 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -516,23 +513,25 @@ class TestMixedModuleStore(unittest.TestCase):
""" """
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
vertical_children_num = len(self.vertical_x1a.children)
vertical = self.store.get_item(self.vertical_x1a)
vertical_children_num = len(vertical.children)
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
# delete leaf problem (will make parent vertical a draft) # delete leaf problem (will make parent vertical a draft)
self.store.delete_item(self.problem_x1a_1.location, self.user_id) self.store.delete_item(self.problem_x1a_1, self.user_id)
draft_parent = self.store.get_item(self.vertical_x1a.location) draft_parent = self.store.get_item(self.vertical_x1a)
self.assertEqual(vertical_children_num - 1, len(draft_parent.children)) self.assertEqual(vertical_children_num - 1, len(draft_parent.children))
published_parent = self.store.get_item( published_parent = self.store.get_item(
self.vertical_x1a.location, self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.published_only revision=ModuleStoreEnum.RevisionOption.published_only
) )
self.assertEqual(vertical_children_num, len(published_parent.children)) self.assertEqual(vertical_children_num, len(published_parent.children))
self.store.revert_to_published(self.vertical_x1a.location, self.user_id) self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_parent = self.store.get_item(self.vertical_x1a.location) 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.assertEqual(reverted_parent, published_parent)
...@@ -545,14 +544,15 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -545,14 +544,15 @@ class TestMixedModuleStore(unittest.TestCase):
self._create_block_hierarchy() self._create_block_hierarchy()
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
orig_display_name = self.problem_x1a_1.display_name problem = self.store.get_item(self.problem_x1a_1)
orig_display_name = problem.display_name
# Change display name of problem and update just it (so parent remains published) # Change display name of problem and update just it (so parent remains published)
self.problem_x1a_1.display_name = "updated before calling revert" problem.display_name = "updated before calling revert"
self.store.update_item(self.problem_x1a_1, self.user_id) self.store.update_item(problem, self.user_id)
self.store.revert_to_published(self.vertical_x1a.location, self.user_id) self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_problem = self.store.get_item(self.problem_x1a_1.location) reverted_problem = self.store.get_item(self.problem_x1a_1)
self.assertEqual(orig_display_name, reverted_problem.display_name) self.assertEqual(orig_display_name, reverted_problem.display_name)
@ddt.data('draft') @ddt.data('draft')
...@@ -564,9 +564,9 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -564,9 +564,9 @@ class TestMixedModuleStore(unittest.TestCase):
self._create_block_hierarchy() self._create_block_hierarchy()
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
orig_vertical = self.vertical_x1a orig_vertical = self.store.get_item(self.vertical_x1a)
self.store.revert_to_published(self.vertical_x1a.location, self.user_id) self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_vertical = self.store.get_item(self.vertical_x1a.location) reverted_vertical = self.store.get_item(self.vertical_x1a)
self.assertEqual(orig_vertical, reverted_vertical) self.assertEqual(orig_vertical, reverted_vertical)
@ddt.data('draft') @ddt.data('draft')
...@@ -577,7 +577,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -577,7 +577,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
with self.assertRaises(InvalidVersionError): with self.assertRaises(InvalidVersionError):
self.store.revert_to_published(self.vertical_x1a.location, self.user_id) self.store.revert_to_published(self.vertical_x1a, self.user_id)
@ddt.data('draft') @ddt.data('draft')
def test_revert_to_published_direct_only(self, default_ms): def test_revert_to_published_direct_only(self, default_ms):
...@@ -586,8 +586,8 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -586,8 +586,8 @@ class TestMixedModuleStore(unittest.TestCase):
""" """
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
self.store.revert_to_published(self.sequential_x1.location, self.user_id) self.store.revert_to_published(self.sequential_x1, self.user_id)
reverted_parent = self.store.get_item(self.sequential_x1.location) reverted_parent = self.store.get_item(self.sequential_x1)
# It does not discard the child vertical, even though that child is a draft (with no published version) # It does not discard the child vertical, even though that child is a draft (with no published version)
self.assertEqual(1, len(reverted_parent.children)) self.assertEqual(1, len(reverted_parent.children))
...@@ -596,6 +596,9 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -596,6 +596,9 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms) self.initdb(default_ms)
course_id = self.course_locations[self.MONGO_COURSEID].course_key course_id = self.course_locations[self.MONGO_COURSEID].course_key
# create parented children
self._create_block_hierarchy()
# orphans # orphans
orphan_locations = [ orphan_locations = [
course_id.make_usage_key('chapter', 'OrphanChapter'), course_id.make_usage_key('chapter', 'OrphanChapter'),
...@@ -612,7 +615,12 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -612,7 +615,12 @@ class TestMixedModuleStore(unittest.TestCase):
] ]
for location in (orphan_locations + detached_locations): for location in (orphan_locations + detached_locations):
self.store.create_item(self.user_id, location) self.store.create_item(
self.user_id,
location.course_key,
location.block_type,
block_id=location.block_id
)
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key) found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(set(found_orphans), set(orphan_locations)) self.assertEqual(set(found_orphans), set(orphan_locations))
...@@ -624,9 +632,11 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -624,9 +632,11 @@ class TestMixedModuleStore(unittest.TestCase):
new location for the child new location for the child
""" """
self.initdb(default_ms) self.initdb(default_ms)
self.store.create_item( self.store.create_child(
self.user_id, parent_location=self.course_locations[self.MONGO_COURSEID], self.user_id,
category='problem', block_id='orphan' self.course_locations[self.MONGO_COURSEID],
'problem',
block_id='orphan'
) )
orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key) orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans)) self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans))
...@@ -655,32 +665,80 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -655,32 +665,80 @@ class TestMixedModuleStore(unittest.TestCase):
""" """
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
# publish # publish
self.store.publish(self.course.location, self.user_id) self.store.publish(self.course.location, self.user_id)
published_xblock = self.store.get_item( published_xblock = self.store.get_item(
self.vertical_x1a.location.replace(branch=None), self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.published_only revision=ModuleStoreEnum.RevisionOption.published_only
) )
self.assertIsNotNone(published_xblock) self.assertIsNotNone(published_xblock)
# unpublish # unpublish
self.store.unpublish(self.vertical_x1a.location, self.user_id) self.store.unpublish(self.vertical_x1a, self.user_id)
with self.assertRaises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_item( self.store.get_item(
self.vertical_x1a.location.replace(branch=None), self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.published_only revision=ModuleStoreEnum.RevisionOption.published_only
) )
draft_xblock_from_get_item = self.store.get_item( # make sure draft version still exists
self.vertical_x1a.location.replace(branch=None), draft_xblock = self.store.get_item(
self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.draft_only revision=ModuleStoreEnum.RevisionOption.draft_only
) )
self.assertIsNotNone(draft_xblock)
@ddt.data('draft', 'split')
def test_compute_publish_state(self, default_ms):
"""
Test the compute_publish_state method
"""
self.initdb(default_ms)
self._create_block_hierarchy()
# TODO - Remove this call to explicitly Publish the course once LMS-2869 is implemented
# For now, we need this since we can't publish a child item without its course already been published
course_location = self.course_locations[self.MONGO_COURSEID]
self.store.publish(course_location, self.user_id)
# start off as Private
item = self.store.create_child(self.user_id, self.writable_chapter_location, 'problem', 'test_compute_publish_state')
item_location = item.location.version_agnostic()
self.assertEquals(self.store.compute_publish_state(item), PublishState.private)
# Private -> Public
self.store.publish(item_location, self.user_id)
item = self.store.get_item(item_location)
self.assertEquals(self.store.compute_publish_state(item), PublishState.public)
# Public -> Private
self.store.unpublish(item_location, self.user_id)
item = self.store.get_item(item_location)
self.assertEquals(self.store.compute_publish_state(item), PublishState.private)
# Private -> Public
self.store.publish(item_location, self.user_id)
item = self.store.get_item(item_location)
self.assertEquals(self.store.compute_publish_state(item), PublishState.public)
# Public -> Draft with NO changes
# Note: This is where Split and Mongo differ
self.store.convert_to_draft(item_location, self.user_id)
item = self.store.get_item(item_location)
self.assertEquals( self.assertEquals(
published_xblock.location.version_agnostic().replace(branch=None), self.store.compute_publish_state(item),
draft_xblock_from_get_item.location.version_agnostic().replace(branch=None) PublishState.draft if default_ms == 'draft' else PublishState.public
) )
# Draft WITH changes
item.display_name = 'new name'
item = self.store.update_item(item, self.user_id)
self.assertTrue(self.store.has_changes(item.location))
self.assertEquals(self.store.compute_publish_state(item), PublishState.draft)
#============================================================================================================= #=============================================================================================================
# General utils for not using django settings # General utils for not using django settings
#============================================================================================================= #=============================================================================================================
......
...@@ -391,12 +391,27 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -391,12 +391,27 @@ class TestMongoModuleStore(unittest.TestCase):
course = self.draft_store.get_course(course_key) course = self.draft_store.get_course(course_key)
# can't use item factory as it depends on django settings # can't use item factory as it depends on django settings
p1ele = self.draft_store.create_item( p1ele = self.draft_store.create_item(
99, course.id.make_usage_key('problem', 'p1'), runtime=course.runtime) 99,
course_key,
'problem',
block_id='p1',
runtime=course.runtime
)
p2ele = self.draft_store.create_item( p2ele = self.draft_store.create_item(
99, course.id.make_usage_key('problem', 'p2'), runtime=course.runtime) 99,
course_key,
'problem',
block_id='p2',
runtime=course.runtime
)
self.refloc = course.id.make_usage_key('ref_test', 'ref_test') self.refloc = course.id.make_usage_key('ref_test', 'ref_test')
self.draft_store.create_item( self.draft_store.create_item(
99,self.refloc, runtime=course.runtime, fields={ 99,
self.refloc.course_key,
self.refloc.block_type,
block_id=self.refloc.block_id,
runtime=course.runtime,
fields={
'reference_link': p1ele.location, 'reference_link': p1ele.location,
'reference_list': [p1ele.location, p2ele.location], 'reference_list': [p1ele.location, p2ele.location],
'reference_dict': {'p1': p1ele.location, 'p2': p2ele.location}, 'reference_dict': {'p1': p1ele.location, 'p2': p2ele.location},
...@@ -497,12 +512,16 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -497,12 +512,16 @@ class TestMongoModuleStore(unittest.TestCase):
""" """
Tests that has_changes() returns false when a new xblock in a direct only category is checked Tests that has_changes() returns false when a new xblock in a direct only category is checked
""" """
course_location = Location('edx', 'direct', '2012_Fall', 'course', 'test_course') course_location = Location('edX', 'toy', '2012_Fall', 'course', '2012_Fall')
chapter_location = Location('edx', 'direct', '2012_Fall', 'chapter', 'test_chapter') chapter_location = Location('edX', 'toy', '2012_Fall', 'chapter', 'vertical_container')
# Create dummy direct only xblocks # Create dummy direct only xblocks
self.draft_store.create_item(self.dummy_user, course_location) self.draft_store.create_item(
self.draft_store.create_item(self.dummy_user, chapter_location) self.dummy_user,
chapter_location.course_key,
chapter_location.block_type,
block_id=chapter_location.block_id
)
# Check that neither xblock has changes # Check that neither xblock has changes
self.assertFalse(self.draft_store.has_changes(course_location)) self.assertFalse(self.draft_store.has_changes(course_location))
...@@ -512,10 +531,15 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -512,10 +531,15 @@ class TestMongoModuleStore(unittest.TestCase):
""" """
Tests that has_changes() only returns true when changes are present Tests that has_changes() only returns true when changes are present
""" """
location = Location('edX', 'changes', '2012_Fall', 'vertical', 'test_vertical') location = Location('edX', 'toy', '2012_Fall', 'vertical', 'test_vertical')
# Create a dummy component to test against # Create a dummy component to test against
self.draft_store.create_item(self.dummy_user, location) self.draft_store.create_item(
self.dummy_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Not yet published, so changes are present # Not yet published, so changes are present
self.assertTrue(self.draft_store.has_changes(location)) self.assertTrue(self.draft_store.has_changes(location))
...@@ -538,11 +562,16 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -538,11 +562,16 @@ class TestMongoModuleStore(unittest.TestCase):
""" """
Tests that has_changes() returns False when a published parent points to a child that doesn't exist. Tests that has_changes() returns False when a published parent points to a child that doesn't exist.
""" """
location = Location('edX', 'missing', '2012_Fall', 'sequential', 'parent') location = Location('edX', 'toy', '2012_Fall', 'sequential', 'parent')
# Create the parent and point it to a fake child # Create the parent and point it to a fake child
parent = self.draft_store.create_item(self.dummy_user, location) parent = self.draft_store.create_item(
parent.children += [Location('edX', 'missing', '2012_Fall', 'vertical', 'does_not_exist')] self.dummy_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
parent.children += [Location('edX', 'toy', '2012_Fall', 'vertical', 'does_not_exist')]
self.draft_store.update_item(parent, self.dummy_user) self.draft_store.update_item(parent, self.dummy_user)
# Check the parent for changes should return False and not throw an exception # Check the parent for changes should return False and not throw an exception
...@@ -561,27 +590,39 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -561,27 +590,39 @@ class TestMongoModuleStore(unittest.TestCase):
if user_id is None: if user_id is None:
user_id = self.dummy_user user_id = self.dummy_user
locations = { org = 'edX'
'grandparent': Location('edX', 'tree', name, 'chapter', 'grandparent'), course = 'tree{}'.format(name)
'parent_sibling': Location('edX', 'tree', name, 'sequential', 'parent_sibling'), run = name
'parent': Location('edX', 'tree', name, 'sequential', 'parent'),
'child_sibling': Location('edX', 'tree', name, 'vertical', 'child_sibling'),
'child': Location('edX', 'tree', name, 'vertical', 'child'),
}
for key in locations: if not self.draft_store.has_course(SlashSeparatedCourseKey(org, course, run)):
self.draft_store.create_item(user_id, locations[key]) self.draft_store.create_course(org, course, run, user_id)
grandparent = self.draft_store.get_item(locations['grandparent']) locations = {
grandparent.children += [locations['parent_sibling'], locations['parent']] 'grandparent': Location(org, course, run, 'chapter', 'grandparent'),
self.draft_store.update_item(grandparent, user_id=user_id) 'parent_sibling': Location(org, course, run, 'sequential', 'parent_sibling'),
'parent': Location(org, course, run, 'sequential', 'parent'),
'child_sibling': Location(org, course, run, 'vertical', 'child_sibling'),
'child': Location(org, course, run, 'vertical', 'child'),
}
parent = self.draft_store.get_item(locations['parent']) for key in locations:
parent.children += [locations['child_sibling'], locations['child']] self.draft_store.create_item(
self.draft_store.update_item(parent, user_id=user_id) user_id,
locations[key].course_key,
locations[key].block_type,
block_id=locations[key].block_id
)
grandparent = self.draft_store.get_item(locations['grandparent'])
grandparent.children += [locations['parent_sibling'], locations['parent']]
self.draft_store.update_item(grandparent, user_id=user_id)
self.draft_store.publish(locations['parent'], user_id) parent = self.draft_store.get_item(locations['parent'])
self.draft_store.publish(locations['parent_sibling'], user_id) parent.children += [locations['child_sibling'], locations['child']]
self.draft_store.update_item(parent, user_id=user_id)
self.draft_store.publish(locations['parent'], user_id)
self.draft_store.publish(locations['parent_sibling'], user_id)
return locations return locations
...@@ -663,10 +704,12 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -663,10 +704,12 @@ class TestMongoModuleStore(unittest.TestCase):
# Create a new child and attach it to parent # Create a new child and attach it to parent
new_child_location = Location('edX', 'tree', 'has_changes_add_remove_child', 'vertical', 'new_child') new_child_location = Location('edX', 'tree', 'has_changes_add_remove_child', 'vertical', 'new_child')
self.draft_store.create_item(self.dummy_user, new_child_location) self.draft_store.create_child(
parent = self.draft_store.get_item(locations['parent']) self.dummy_user,
parent.children += [new_child_location] locations['parent'],
self.draft_store.update_item(parent, user_id=self.dummy_user) new_child_location.block_type,
block_id=new_child_location.block_id
)
# Verify that the ancestors now have changes # Verify that the ancestors now have changes
self.assertTrue(self.draft_store.has_changes(locations['grandparent'])) self.assertTrue(self.draft_store.has_changes(locations['grandparent']))
...@@ -685,13 +728,21 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -685,13 +728,21 @@ class TestMongoModuleStore(unittest.TestCase):
""" """
Tests that has_changes() returns true after editing the child of a vertical (both not direct only categories). Tests that has_changes() returns true after editing the child of a vertical (both not direct only categories).
""" """
parent_location = Location('edX', 'test', 'non_direct_only_children', 'vertical', 'parent') parent_location = Location('edX', 'toy', '2012_Fall', 'vertical', 'parent')
child_location = Location('edX', 'test', 'non_direct_only_children', 'html', 'child') child_location = Location('edX', 'toy', '2012_Fall', 'html', 'child')
parent = self.draft_store.create_item(self.dummy_user, parent_location) parent = self.draft_store.create_item(
child = self.draft_store.create_item(self.dummy_user, child_location) self.dummy_user,
parent.children += [child_location] parent_location.course_key,
self.draft_store.update_item(parent, user_id=self.dummy_user) parent_location.block_type,
block_id=parent_location.block_id
)
child = self.draft_store.create_child(
self.dummy_user,
parent_location,
child_location.block_type,
block_id=child_location.block_id
)
self.draft_store.publish(parent_location, self.dummy_user) self.draft_store.publish(parent_location, self.dummy_user)
# Verify that there are no changes # Verify that there are no changes
...@@ -757,10 +808,15 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -757,10 +808,15 @@ class TestMongoModuleStore(unittest.TestCase):
""" """
Tests that edited_on and edited_by are set correctly during an update Tests that edited_on and edited_by are set correctly during an update
""" """
location = Location('edX', 'editInfoTest', '2012_Fall', 'html', 'test_html') location = Location('edX', 'toy', '2012_Fall', 'html', 'test_html')
# Create a dummy component to test against # Create a dummy component to test against
self.draft_store.create_item(self.dummy_user, location) self.draft_store.create_item(
self.dummy_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Store the current edit time and verify that dummy_user created the component # Store the current edit time and verify that dummy_user created the component
component = self.draft_store.get_item(location) component = self.draft_store.get_item(location)
...@@ -780,12 +836,17 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -780,12 +836,17 @@ class TestMongoModuleStore(unittest.TestCase):
""" """
Tests that published_date and published_by are set correctly Tests that published_date and published_by are set correctly
""" """
location = Location('edX', 'publishInfo', '2012_Fall', 'html', 'test_html') location = Location('edX', 'toy', '2012_Fall', 'html', 'test_html')
create_user = 123 create_user = 123
publish_user = 456 publish_user = 456
# Create a dummy component to test against # Create a dummy component to test against
self.draft_store.create_item(create_user, location) self.draft_store.create_item(
create_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Store the current time, then publish # Store the current time, then publish
old_time = datetime.now(UTC) old_time = datetime.now(UTC)
......
...@@ -19,7 +19,7 @@ class TestPublish(SplitWMongoCourseBoostrapper): ...@@ -19,7 +19,7 @@ class TestPublish(SplitWMongoCourseBoostrapper):
# There are 12 created items and 7 parent updates # There are 12 created items and 7 parent updates
# create course: finds: 1 to verify uniqueness, 1 to find parents # create course: finds: 1 to verify uniqueness, 1 to find parents
# sends: 1 to create course, 1 to create overview # sends: 1 to create course, 1 to create overview
with check_mongo_calls(self.draft_mongo, 5, 2): with check_mongo_calls(self.draft_mongo, 6, 2):
super(TestPublish, self)._create_course(split=False) # 2 inserts (course and overview) super(TestPublish, self)._create_course(split=False) # 2 inserts (course and overview)
# with bulk will delay all inheritance computations which won't be added into the mongo_calls # with bulk will delay all inheritance computations which won't be added into the mongo_calls
......
...@@ -30,8 +30,7 @@ class TestDraftVersioningModuleStore(unittest.TestCase): ...@@ -30,8 +30,7 @@ class TestDraftVersioningModuleStore(unittest.TestCase):
render_template=render_to_template_mock, render_template=render_to_template_mock,
xblock_mixins=(InheritanceMixin, XModuleMixin), xblock_mixins=(InheritanceMixin, XModuleMixin),
) )
# NAATODO - uncomment once merged with drop_database PR self.addCleanup(self.module_store._drop_database)
# self.addCleanup(module_store._drop_database)
SplitModuleTest.bootstrapDB(self.module_store) SplitModuleTest.bootstrapDB(self.module_store)
......
...@@ -444,7 +444,7 @@ class SplitModuleTest(unittest.TestCase): ...@@ -444,7 +444,7 @@ class SplitModuleTest(unittest.TestCase):
} }
}, },
} }
@staticmethod @staticmethod
def bootstrapDB(split_store): def bootstrapDB(split_store):
''' '''
...@@ -985,7 +985,7 @@ class TestItemCrud(SplitModuleTest): ...@@ -985,7 +985,7 @@ class TestItemCrud(SplitModuleTest):
# add minimal one w/o a parent # add minimal one w/o a parent
category = 'sequential' category = 'sequential'
new_module = modulestore().create_item( new_module = modulestore().create_item(
'user123', parent_location=locator, category=category, 'user123', locator, category,
fields={'display_name': 'new sequential'} fields={'display_name': 'new sequential'}
) )
# check that course version changed and course's previous is the other one # check that course version changed and course's previous is the other one
...@@ -1025,8 +1025,8 @@ class TestItemCrud(SplitModuleTest): ...@@ -1025,8 +1025,8 @@ class TestItemCrud(SplitModuleTest):
) )
premod_course = modulestore().get_course(locator.course_key) premod_course = modulestore().get_course(locator.course_key)
category = 'chapter' category = 'chapter'
new_module = modulestore().create_item( new_module = modulestore().create_child(
'user123', parent_location=locator, category=category, 'user123', locator, category,
fields={'display_name': 'new chapter'}, fields={'display_name': 'new chapter'},
definition_locator=original.definition_locator definition_locator=original.definition_locator
) )
...@@ -1054,13 +1054,13 @@ class TestItemCrud(SplitModuleTest): ...@@ -1054,13 +1054,13 @@ class TestItemCrud(SplitModuleTest):
) )
category = 'problem' category = 'problem'
new_payload = "<problem>empty</problem>" new_payload = "<problem>empty</problem>"
new_module = modulestore().create_item( new_module = modulestore().create_child(
'anotheruser', parent_location=locator, category=category, 'anotheruser', locator, category,
fields={'display_name': 'problem 1', 'data': new_payload}, fields={'display_name': 'problem 1', 'data': new_payload},
) )
another_payload = "<problem>not empty</problem>" another_payload = "<problem>not empty</problem>"
another_module = modulestore().create_item( another_module = modulestore().create_child(
'anotheruser', parent_location=locator, category=category, 'anotheruser', locator, category,
fields={'display_name': 'problem 2', 'data': another_payload}, fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=original.definition_locator, definition_locator=original.definition_locator,
) )
...@@ -1086,8 +1086,8 @@ class TestItemCrud(SplitModuleTest): ...@@ -1086,8 +1086,8 @@ class TestItemCrud(SplitModuleTest):
course_key = CourseLocator(org='guestx', course='contender', run="run", branch=BRANCH_NAME_DRAFT) course_key = CourseLocator(org='guestx', course='contender', run="run", branch=BRANCH_NAME_DRAFT)
parent_locator = BlockUsageLocator(course_key, 'course', block_id="head345679") parent_locator = BlockUsageLocator(course_key, 'course', block_id="head345679")
chapter_locator = BlockUsageLocator(course_key, 'chapter', block_id="foo.bar_-~:0") chapter_locator = BlockUsageLocator(course_key, 'chapter', block_id="foo.bar_-~:0")
modulestore().create_item( modulestore().create_child(
'anotheruser', parent_location=parent_locator, category='chapter', 'anotheruser', parent_locator, 'chapter',
block_id=chapter_locator.block_id, block_id=chapter_locator.block_id,
fields={'display_name': 'chapter 99'}, fields={'display_name': 'chapter 99'},
) )
...@@ -1097,8 +1097,8 @@ class TestItemCrud(SplitModuleTest): ...@@ -1097,8 +1097,8 @@ class TestItemCrud(SplitModuleTest):
# now try making that a parent of something # now try making that a parent of something
new_payload = "<problem>empty</problem>" new_payload = "<problem>empty</problem>"
problem_locator = BlockUsageLocator(course_key, 'problem', block_id="prob.bar_-~:99a") problem_locator = BlockUsageLocator(course_key, 'problem', block_id="prob.bar_-~:99a")
modulestore().create_item( modulestore().create_child(
'anotheruser', parent_location=chapter_locator, category='problem', 'anotheruser', chapter_locator, 'problem',
block_id=problem_locator.block_id, block_id=problem_locator.block_id,
fields={'display_name': 'chapter 99', 'data': new_payload}, fields={'display_name': 'chapter 99', 'data': new_payload},
) )
...@@ -1123,8 +1123,8 @@ class TestItemCrud(SplitModuleTest): ...@@ -1123,8 +1123,8 @@ class TestItemCrud(SplitModuleTest):
versionless_course_locator = new_course_locator.version_agnostic() versionless_course_locator = new_course_locator.version_agnostic()
# positive simple case: no force, add chapter # positive simple case: no force, add chapter
new_ele = modulestore().create_item( new_ele = modulestore().create_child(
user, parent_location=new_course.location, category='chapter', user, new_course.location, 'chapter',
fields={'display_name': 'chapter 1'}, fields={'display_name': 'chapter 1'},
continue_version=True continue_version=True
) )
...@@ -1141,40 +1141,40 @@ class TestItemCrud(SplitModuleTest): ...@@ -1141,40 +1141,40 @@ class TestItemCrud(SplitModuleTest):
# try to create existing item # try to create existing item
with self.assertRaises(DuplicateItemError): with self.assertRaises(DuplicateItemError):
_fail = modulestore().create_item( _fail = modulestore().create_child(
user, parent_location=new_course.location, category='chapter', user, new_course.location, 'chapter',
block_id=new_ele.location.block_id, block_id=new_ele.location.block_id,
fields={'display_name': 'chapter 2'}, fields={'display_name': 'chapter 2'},
continue_version=True continue_version=True
) )
# start a new transaction # start a new transaction
new_ele = modulestore().create_item( new_ele = modulestore().create_child(
user, parent_location=new_course.location, category='chapter', user, new_course.location, 'chapter',
fields={'display_name': 'chapter 2'}, fields={'display_name': 'chapter 2'},
continue_version=False continue_version=False
) )
transaction_guid = new_ele.location.version_guid transaction_guid = new_ele.location.version_guid
# ensure force w/ continue gives exception # ensure force w/ continue gives exception
with self.assertRaises(VersionConflictError): with self.assertRaises(VersionConflictError):
_fail = modulestore().create_item( _fail = modulestore().create_child(
user, parent_location=new_course.location, category='chapter', user, new_course.location, 'chapter',
fields={'display_name': 'chapter 2'}, fields={'display_name': 'chapter 2'},
force=True, continue_version=True force=True, continue_version=True
) )
# ensure trying to continue the old one gives exception # ensure trying to continue the old one gives exception
with self.assertRaises(VersionConflictError): with self.assertRaises(VersionConflictError):
_fail = modulestore().create_item( _fail = modulestore().create_child(
user, parent_location=new_course.location, category='chapter', user, new_course.location, 'chapter',
fields={'display_name': 'chapter 3'}, fields={'display_name': 'chapter 3'},
continue_version=True continue_version=True
) )
# add new child to old parent in continued (leave off version_guid) # add new child to old parent in continued (leave off version_guid)
course_module_locator = new_course.location.version_agnostic() course_module_locator = new_course.location.version_agnostic()
new_ele = modulestore().create_item( new_ele = modulestore().create_child(
user, parent_location=course_module_locator, category='chapter', user, course_module_locator, 'chapter',
fields={'display_name': 'chapter 4'}, fields={'display_name': 'chapter 4'},
continue_version=True continue_version=True
) )
...@@ -1283,13 +1283,13 @@ class TestItemCrud(SplitModuleTest): ...@@ -1283,13 +1283,13 @@ class TestItemCrud(SplitModuleTest):
) )
category = 'problem' category = 'problem'
new_payload = "<problem>empty</problem>" new_payload = "<problem>empty</problem>"
modulestore().create_item( modulestore().create_child(
'test_update_manifold', parent_location=locator, category=category, 'test_update_manifold', locator, category,
fields={'display_name': 'problem 1', 'data': new_payload}, fields={'display_name': 'problem 1', 'data': new_payload},
) )
another_payload = "<problem>not empty</problem>" another_payload = "<problem>not empty</problem>"
modulestore().create_item( modulestore().create_child(
'test_update_manifold', parent_location=locator, category=category, 'test_update_manifold', locator, category,
fields={'display_name': 'problem 2', 'data': another_payload}, fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=original.definition_locator, definition_locator=original.definition_locator,
) )
...@@ -1368,8 +1368,8 @@ class TestItemCrud(SplitModuleTest): ...@@ -1368,8 +1368,8 @@ class TestItemCrud(SplitModuleTest):
""" """
if not category_queue: if not category_queue:
return return
node = modulestore().create_item( node = modulestore().create_child(
'deleting_user', parent_location=parent.version_agnostic(), category=category_queue[0] 'deleting_user', parent.version_agnostic(), category_queue[0]
) )
node_loc = node.location.map_into_course(parent.course_key) node_loc = node.location.map_into_course(parent.course_key)
for _ in range(4): for _ in range(4):
...@@ -1433,8 +1433,8 @@ class TestCourseCreation(SplitModuleTest): ...@@ -1433,8 +1433,8 @@ class TestCourseCreation(SplitModuleTest):
# changing this course will not change the original course # changing this course will not change the original course
# using new_draft.location will insert the chapter under the course root # using new_draft.location will insert the chapter under the course root
new_item = modulestore().create_item( new_item = modulestore().create_child(
'leech_master', parent_location=new_draft.location, category='chapter', 'leech_master', new_draft.location, 'chapter',
fields={'display_name': 'new chapter'} fields={'display_name': 'new chapter'}
) )
new_draft_locator = new_draft_locator.course_key.version_agnostic() new_draft_locator = new_draft_locator.course_key.version_agnostic()
...@@ -1595,8 +1595,8 @@ class TestPublish(SplitModuleTest): ...@@ -1595,8 +1595,8 @@ class TestPublish(SplitModuleTest):
source_course, dest_course, expected, [chapter2.block_id, chapter3.block_id, "problem1", "problem3_2"] source_course, dest_course, expected, [chapter2.block_id, chapter3.block_id, "problem1", "problem3_2"]
) )
# add a child under chapter1 # add a child under chapter1
new_module = modulestore().create_item( new_module = modulestore().create_child(
self.user_id, parent_location=chapter1, category="sequential", self.user_id, chapter1, "sequential",
fields={'display_name': 'new sequential'}, fields={'display_name': 'new sequential'},
) )
# remove chapter1 from expected b/c its pub'd version != the source anymore since source changed # remove chapter1 from expected b/c its pub'd version != the source anymore since source changed
...@@ -1614,7 +1614,7 @@ class TestPublish(SplitModuleTest): ...@@ -1614,7 +1614,7 @@ class TestPublish(SplitModuleTest):
) )
# ensure intentionally orphaned blocks work (e.g., course_info) # ensure intentionally orphaned blocks work (e.g., course_info)
new_module = modulestore().create_item( new_module = modulestore().create_item(
self.user_id, parent_location=source_course, category="course_info", block_id="handouts" self.user_id, source_course, "course_info", block_id="handouts"
) )
# publish it # publish it
modulestore().copy(self.user_id, source_course, dest_course, [new_module.location], None) modulestore().copy(self.user_id, source_course, dest_course, [new_module.location], None)
......
...@@ -87,7 +87,13 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase): ...@@ -87,7 +87,13 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
""" """
location = self.old_course_key.make_usage_key(category, name) location = self.old_course_key.make_usage_key(category, name)
self.draft_mongo.create_item( self.draft_mongo.create_item(
self.user_id, location, definition_data=data, metadata=metadata, runtime=self.runtime self.user_id,
location.course_key,
location.block_type,
block_id=location.block_id,
definition_data=data,
metadata=metadata,
runtime=self.runtime
) )
if not draft: if not draft:
self.draft_mongo.publish(location, self.user_id) self.draft_mongo.publish(location, self.user_id)
...@@ -104,16 +110,28 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase): ...@@ -104,16 +110,28 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
self.draft_mongo.update_item(parent, self.user_id) self.draft_mongo.update_item(parent, self.user_id)
if not draft: if not draft:
self.draft_mongo.publish(parent_location, self.user_id) self.draft_mongo.publish(parent_location, self.user_id)
# create pointer for split # create child for split
course_or_parent_locator = BlockUsageLocator( if split:
course_key=self.split_course_key, self.split_mongo.create_child(
block_type=parent_category, self.user_id,
block_id=parent_name BlockUsageLocator(
) course_key=self.split_course_key,
block_type=parent_category,
block_id=parent_name
),
category,
block_id=name,
fields=fields
)
else: else:
course_or_parent_locator = self.split_course_key if split:
if split: self.split_mongo.create_item(
self.split_mongo.create_item(course_or_parent_locator, category, self.user_id, block_id=name, fields=fields) self.user_id,
self.split_course_key,
category,
block_id=name,
fields=fields
)
def _create_course(self, split=True): def _create_course(self, split=True):
""" """
......
...@@ -578,7 +578,9 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -578,7 +578,9 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
metadata = {'display_name': group.name} metadata = {'display_name': group.name}
modulestore.create_item( modulestore.create_item(
user_id, user_id,
dest_usage_key, self.location.course_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
definition_data=None, definition_data=None,
metadata=metadata, metadata=metadata,
runtime=self.system, runtime=self.system,
......
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