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):
try:
course_updates = modulestore().get_item(location)
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)
return _get_visible_update(course_update_items)
......@@ -51,7 +51,7 @@ def update_course_updates(location, update, passed_id=None, user=None):
try:
course_updates = modulestore().get_item(location)
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)))
......
......@@ -575,7 +575,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
location = course.id.make_usage_key('chapter', 'neuvo')
# 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))
with self.assertRaises(InvalidVersionError):
draft_store.convert_to_draft(location, self.user.id)
......@@ -1392,12 +1392,9 @@ class ContentStoreTest(ContentStoreTestCase):
def test_forum_id_generation(self):
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
self.store.create_item(self.user.id, new_component_location)
new_discussion_item = self.store.get_item(new_component_location)
new_discussion_item = self.store.create_item(self.user.id, course.id, 'discussion', 'new_component')
self.assertNotEquals(new_discussion_item.discussion_id, '$$GUID$$')
......
......@@ -162,7 +162,7 @@ class TemplateTests(unittest.TestCase):
self.assertIsInstance(self.split_store.get_course(id_locator), CourseDescriptor)
# and by guid
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
self.assertRaises(ItemNotFoundError, self.split_store.get_course, id_locator)
# but can by guid
......@@ -187,11 +187,11 @@ class TemplateTests(unittest.TestCase):
)
first_problem.max_attempts = 3
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.assertEqual(updated_problem.previous_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(
display_name='problem 2',
......
......@@ -34,7 +34,13 @@ class TestOrphan(CourseTestCase):
location = self.course.location.replace(category=category, name=name)
store = modulestore()
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:
# add child to parent in mongo
......
......@@ -151,11 +151,11 @@ class CourseTestCase(ModuleStoreTestCase):
self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft)
# 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)
# 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)
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
if usage_key.category in CREATE_IF_NOT_FOUND:
# New module at this location, for pages that are not pre-created.
# 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:
raise
except InvalidLocationError:
......@@ -416,9 +416,11 @@ def _create_item(request):
if display_name is not None:
metadata['display_name'] = display_name
created_block = store.create_item(
created_block = store.create_child(
request.user.id,
dest_usage_key,
usage_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
definition_data=data,
metadata=metadata,
runtime=parent.runtime,
......@@ -437,11 +439,6 @@ def _create_item(request):
)
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)})
......@@ -467,7 +464,9 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
dest_module = store.create_item(
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),
metadata=duplicate_metadata,
runtime=source_item.runtime,
......@@ -555,7 +554,7 @@ def _get_module_info(usage_key, user, rewrite_static_links=True):
except ItemNotFoundError:
if usage_key.category in CREATE_IF_NOT_FOUND:
# 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:
raise
......
......@@ -130,7 +130,12 @@ class CourseUpdateTest(CourseTestCase):
'''
# get the updates and populate 'data' field with some data.
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_content = u"Hello world!"
update_data = u"<ol><li><h2>" + update_date + "</h2>" + update_content + "</li></ol>"
......@@ -204,7 +209,12 @@ class CourseUpdateTest(CourseTestCase):
'''Test trying to add to a saved course_update which is not an ol.'''
# get the updates and set to something wrong
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.data = 'bad news'
modulestore().update_item(course_updates, self.user.id)
......@@ -229,8 +239,7 @@ class CourseUpdateTest(CourseTestCase):
"""
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=course_key)
course_update_url = self.create_update_url(course_key=self.course.id)
# create a course via the view handler
self.client.ajax_post(course_update_url)
......
......@@ -368,6 +368,25 @@ class ModuleStoreWrite(ModuleStoreRead):
pass
@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):
"""
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):
result[field.scope][field_name] = value
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):
"""
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):
self.contentstore._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
def bulk_write_operations(self, course_id):
"""
......@@ -699,18 +691,3 @@ class EdxJSONEncoder(json.JSONEncoder):
return obj.isoformat()
else:
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):
"""
__metaclass__ = ABCMeta
def __init__(self, **kwargs):
super(ModuleStoreDraftAndPublished, self).__init__(**kwargs)
def __init__(self, *args, **kwargs):
super(ModuleStoreDraftAndPublished, self).__init__(*args, **kwargs)
self.branch_setting_func = kwargs.pop('branch_setting_func', lambda: ModuleStoreEnum.Branch.published_only)
@abstractmethod
......
......@@ -7,21 +7,23 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule
import logging
from contextlib import contextmanager
from opaque_keys import InvalidKeyError
import itertools
from . import ModuleStoreWriteBase
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
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__)
class MixedModuleStore(ModuleStoreWriteBase):
class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
"""
ModuleStore knows how to route requests to the right persistence ms
"""
......@@ -282,7 +284,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
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)
def clone_course(self, source_course_id, dest_course_id, user_id):
......@@ -312,40 +314,57 @@ class MixedModuleStore(ModuleStoreWriteBase):
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.
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.
Creates and saves a new item in a course.
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
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
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)
modulestore = self._verify_modulestore_support(location, 'create_item')
return modulestore.create_item(user_id, location, parent_location, **kwargs)
Creates and saves a new xblock that is 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
"""
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):
"""
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.
"""
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)
def delete_item(self, location, user_id, **kwargs):
"""
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)
def revert_to_published(self, location, user_id):
......@@ -358,7 +377,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
: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)
def close_all_connections(self):
......@@ -388,7 +407,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
: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
"""
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)
def get_courses_for_wiki(self, wiki_slug):
......@@ -433,7 +452,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Save a current draft to the underlying modulestore
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)
def unpublish(self, location, user_id):
......@@ -441,7 +460,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Save a current draft to the underlying modulestore
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)
def convert_to_draft(self, location, user_id):
......@@ -451,18 +470,26 @@ class MixedModuleStore(ModuleStoreWriteBase):
: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)
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
that the store supports 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_id)
store = self._get_modulestore_for_courseid(course_key)
if hasattr(store, method):
return store
else:
......
......@@ -17,6 +17,7 @@ import sys
import logging
import copy
import re
from uuid import uuid4
from bson.son import SON
from fs.osfs import OSFS
......@@ -904,7 +905,12 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
]))
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
about_location = location.replace(
......@@ -914,7 +920,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
overview_template = AboutDescriptor.get_template('overview.yaml')
self.create_item(
user_id,
about_location,
about_location.course_key,
about_location.block_type,
block_id=about_location.block_id,
definition_data=overview_template.get('data'),
runtime=course.system
)
......@@ -975,6 +983,52 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
xmodule.save()
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):
'''
for a given Xmodule, return the course that it belongs to
......@@ -1065,6 +1119,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
except ItemNotFoundError:
if not allow_not_found:
raise
elif not self.has_course(xblock.location.course_key):
raise ItemNotFoundError(xblock.location.course_key)
return xblock
......
......@@ -11,7 +11,7 @@ import logging
from opaque_keys.edx.locations import Location
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 (
ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError
)
......@@ -305,35 +305,6 @@ class DraftModuleStore(MongoModuleStore):
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):
"""
Performance Note: This is generally a costly operation, but useful for wildcard searches.
......@@ -511,6 +482,7 @@ class DraftModuleStore(MongoModuleStore):
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.
"""
self._verify_branch_setting(ModuleStoreEnum.Branch.draft_preferred)
_verify_revision_is_published(location)
......@@ -555,8 +527,10 @@ class DraftModuleStore(MongoModuleStore):
as_functions = [as_draft, as_published]
elif revision == ModuleStoreEnum.RevisionOption.published_only:
as_functions = [as_published]
else:
elif revision is None:
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)
def _delete_subtree(self, location, as_functions):
......
......@@ -85,8 +85,8 @@ class SplitMigrator(object):
# the 'children' field as it goes.
_new_module = self.split_modulestore.create_item(
user_id,
course_version_locator.make_usage_key(module.location.category, module.location.block_id),
parent_location=course_version_locator,
course_version_locator,
module.location.block_type,
block_id=module.location.block_id,
fields=self._get_json_fields_translate_references(
module, course_version_locator, new_course.location.block_id
......@@ -131,7 +131,8 @@ class SplitMigrator(object):
else:
# only a draft version (aka, 'private').
_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,
fields=self._get_json_fields_translate_references(
module, new_draft_course_loc, published_course_usage_key.block_id
......
......@@ -6,6 +6,7 @@ from ..exceptions import ItemNotFoundError
from split import SplitMongoModuleStore
from xmodule.modulestore import ModuleStoreEnum, PublishState
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.draft import DIRECT_ONLY_CATEGORIES
class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore):
......@@ -13,13 +14,10 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
A subclass of Split that supports a dual-branch fall-back versioning framework
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):
master_branch = kwargs.pop('master_branch', ModuleStoreEnum.BranchName.draft)
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):
......@@ -31,32 +29,81 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
def delete_item(self, location, user_id, revision=None, **kwargs):
"""
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:
branches_to_delete = [ModuleStoreEnum.BranchName.published]
elif revision == ModuleStoreEnum.RevisionOption.all:
branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
else:
elif revision is None:
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:
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:
usage_key = for_branch(ModuleStoreEnum.BranchName.published)
return key.for_branch(ModuleStoreEnum.BranchName.published)
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)
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):
# 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)
def has_changes(self, usage_key):
......@@ -65,6 +112,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
:param usage_key: the block to check
: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))
try:
published = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.published))
......@@ -120,20 +168,21 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
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)
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:
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:
other = get_head(ModuleStoreEnum.BranchName.draft)
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 xblock.location.branch == ModuleStoreEnum.BranchName.draft:
return PublishState.private
else:
return PublishState.public # a bit nonsensical
return PublishState.public
elif xblock.update_version != other['edit_info']['update_version']:
return PublishState.draft
else:
......
......@@ -174,7 +174,15 @@ class ItemFactory(XModuleFactory):
if display_name is not None:
metadata['display_name'] = display_name
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)
......
......@@ -4,6 +4,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.x_module import XModuleDescriptor
import factory
from factory.helpers import lazy_attribute
from opaque_keys.edx.keys import UsageKey
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232, C0111
......@@ -73,10 +74,16 @@ class ItemFactory(SplitFactory):
:param definition_locator (optional): the DescriptorLocator for the definition this uses or branches
"""
modulestore = kwargs.pop('modulestore')
return modulestore.create_item(
user_id, category=category, parent_location=parent_location, defintion_locator=definition_locator,
force=force, continue_version=continue_version, **kwargs
)
if isinstance(parent_location, UsageKey):
return modulestore.create_child(
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
def _build(cls, target_class, *args, **kwargs):
......
......@@ -19,7 +19,7 @@ class TestPublish(SplitWMongoCourseBoostrapper):
# There are 12 created items and 7 parent updates
# create course: finds: 1 to verify uniqueness, 1 to find parents
# 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)
# with bulk will delay all inheritance computations which won't be added into the mongo_calls
......
......@@ -30,8 +30,7 @@ class TestDraftVersioningModuleStore(unittest.TestCase):
render_template=render_to_template_mock,
xblock_mixins=(InheritanceMixin, XModuleMixin),
)
# NAATODO - uncomment once merged with drop_database PR
# self.addCleanup(module_store._drop_database)
self.addCleanup(self.module_store._drop_database)
SplitModuleTest.bootstrapDB(self.module_store)
......
......@@ -444,7 +444,7 @@ class SplitModuleTest(unittest.TestCase):
}
},
}
@staticmethod
def bootstrapDB(split_store):
'''
......@@ -985,7 +985,7 @@ class TestItemCrud(SplitModuleTest):
# add minimal one w/o a parent
category = 'sequential'
new_module = modulestore().create_item(
'user123', parent_location=locator, category=category,
'user123', locator, category,
fields={'display_name': 'new sequential'}
)
# check that course version changed and course's previous is the other one
......@@ -1025,8 +1025,8 @@ class TestItemCrud(SplitModuleTest):
)
premod_course = modulestore().get_course(locator.course_key)
category = 'chapter'
new_module = modulestore().create_item(
'user123', parent_location=locator, category=category,
new_module = modulestore().create_child(
'user123', locator, category,
fields={'display_name': 'new chapter'},
definition_locator=original.definition_locator
)
......@@ -1054,13 +1054,13 @@ class TestItemCrud(SplitModuleTest):
)
category = 'problem'
new_payload = "<problem>empty</problem>"
new_module = modulestore().create_item(
'anotheruser', parent_location=locator, category=category,
new_module = modulestore().create_child(
'anotheruser', locator, category,
fields={'display_name': 'problem 1', 'data': new_payload},
)
another_payload = "<problem>not empty</problem>"
another_module = modulestore().create_item(
'anotheruser', parent_location=locator, category=category,
another_module = modulestore().create_child(
'anotheruser', locator, category,
fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=original.definition_locator,
)
......@@ -1086,8 +1086,8 @@ class TestItemCrud(SplitModuleTest):
course_key = CourseLocator(org='guestx', course='contender', run="run", branch=BRANCH_NAME_DRAFT)
parent_locator = BlockUsageLocator(course_key, 'course', block_id="head345679")
chapter_locator = BlockUsageLocator(course_key, 'chapter', block_id="foo.bar_-~:0")
modulestore().create_item(
'anotheruser', parent_location=parent_locator, category='chapter',
modulestore().create_child(
'anotheruser', parent_locator, 'chapter',
block_id=chapter_locator.block_id,
fields={'display_name': 'chapter 99'},
)
......@@ -1097,8 +1097,8 @@ class TestItemCrud(SplitModuleTest):
# now try making that a parent of something
new_payload = "<problem>empty</problem>"
problem_locator = BlockUsageLocator(course_key, 'problem', block_id="prob.bar_-~:99a")
modulestore().create_item(
'anotheruser', parent_location=chapter_locator, category='problem',
modulestore().create_child(
'anotheruser', chapter_locator, 'problem',
block_id=problem_locator.block_id,
fields={'display_name': 'chapter 99', 'data': new_payload},
)
......@@ -1123,8 +1123,8 @@ class TestItemCrud(SplitModuleTest):
versionless_course_locator = new_course_locator.version_agnostic()
# positive simple case: no force, add chapter
new_ele = modulestore().create_item(
user, parent_location=new_course.location, category='chapter',
new_ele = modulestore().create_child(
user, new_course.location, 'chapter',
fields={'display_name': 'chapter 1'},
continue_version=True
)
......@@ -1141,40 +1141,40 @@ class TestItemCrud(SplitModuleTest):
# try to create existing item
with self.assertRaises(DuplicateItemError):
_fail = modulestore().create_item(
user, parent_location=new_course.location, category='chapter',
_fail = modulestore().create_child(
user, new_course.location, 'chapter',
block_id=new_ele.location.block_id,
fields={'display_name': 'chapter 2'},
continue_version=True
)
# start a new transaction
new_ele = modulestore().create_item(
user, parent_location=new_course.location, category='chapter',
new_ele = modulestore().create_child(
user, new_course.location, 'chapter',
fields={'display_name': 'chapter 2'},
continue_version=False
)
transaction_guid = new_ele.location.version_guid
# ensure force w/ continue gives exception
with self.assertRaises(VersionConflictError):
_fail = modulestore().create_item(
user, parent_location=new_course.location, category='chapter',
_fail = modulestore().create_child(
user, new_course.location, 'chapter',
fields={'display_name': 'chapter 2'},
force=True, continue_version=True
)
# ensure trying to continue the old one gives exception
with self.assertRaises(VersionConflictError):
_fail = modulestore().create_item(
user, parent_location=new_course.location, category='chapter',
_fail = modulestore().create_child(
user, new_course.location, 'chapter',
fields={'display_name': 'chapter 3'},
continue_version=True
)
# add new child to old parent in continued (leave off version_guid)
course_module_locator = new_course.location.version_agnostic()
new_ele = modulestore().create_item(
user, parent_location=course_module_locator, category='chapter',
new_ele = modulestore().create_child(
user, course_module_locator, 'chapter',
fields={'display_name': 'chapter 4'},
continue_version=True
)
......@@ -1283,13 +1283,13 @@ class TestItemCrud(SplitModuleTest):
)
category = 'problem'
new_payload = "<problem>empty</problem>"
modulestore().create_item(
'test_update_manifold', parent_location=locator, category=category,
modulestore().create_child(
'test_update_manifold', locator, category,
fields={'display_name': 'problem 1', 'data': new_payload},
)
another_payload = "<problem>not empty</problem>"
modulestore().create_item(
'test_update_manifold', parent_location=locator, category=category,
modulestore().create_child(
'test_update_manifold', locator, category,
fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=original.definition_locator,
)
......@@ -1368,8 +1368,8 @@ class TestItemCrud(SplitModuleTest):
"""
if not category_queue:
return
node = modulestore().create_item(
'deleting_user', parent_location=parent.version_agnostic(), category=category_queue[0]
node = modulestore().create_child(
'deleting_user', parent.version_agnostic(), category_queue[0]
)
node_loc = node.location.map_into_course(parent.course_key)
for _ in range(4):
......@@ -1433,8 +1433,8 @@ class TestCourseCreation(SplitModuleTest):
# changing this course will not change the original course
# using new_draft.location will insert the chapter under the course root
new_item = modulestore().create_item(
'leech_master', parent_location=new_draft.location, category='chapter',
new_item = modulestore().create_child(
'leech_master', new_draft.location, 'chapter',
fields={'display_name': 'new chapter'}
)
new_draft_locator = new_draft_locator.course_key.version_agnostic()
......@@ -1595,8 +1595,8 @@ class TestPublish(SplitModuleTest):
source_course, dest_course, expected, [chapter2.block_id, chapter3.block_id, "problem1", "problem3_2"]
)
# add a child under chapter1
new_module = modulestore().create_item(
self.user_id, parent_location=chapter1, category="sequential",
new_module = modulestore().create_child(
self.user_id, chapter1, "sequential",
fields={'display_name': 'new sequential'},
)
# remove chapter1 from expected b/c its pub'd version != the source anymore since source changed
......@@ -1614,7 +1614,7 @@ class TestPublish(SplitModuleTest):
)
# ensure intentionally orphaned blocks work (e.g., course_info)
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
modulestore().copy(self.user_id, source_course, dest_course, [new_module.location], None)
......
......@@ -87,7 +87,13 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
"""
location = self.old_course_key.make_usage_key(category, name)
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:
self.draft_mongo.publish(location, self.user_id)
......@@ -104,16 +110,28 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
self.draft_mongo.update_item(parent, self.user_id)
if not draft:
self.draft_mongo.publish(parent_location, self.user_id)
# create pointer for split
course_or_parent_locator = BlockUsageLocator(
course_key=self.split_course_key,
block_type=parent_category,
block_id=parent_name
)
# create child for split
if split:
self.split_mongo.create_child(
self.user_id,
BlockUsageLocator(
course_key=self.split_course_key,
block_type=parent_category,
block_id=parent_name
),
category,
block_id=name,
fields=fields
)
else:
course_or_parent_locator = self.split_course_key
if split:
self.split_mongo.create_item(course_or_parent_locator, category, self.user_id, block_id=name, fields=fields)
if split:
self.split_mongo.create_item(
self.user_id,
self.split_course_key,
category,
block_id=name,
fields=fields
)
def _create_course(self, split=True):
"""
......
......@@ -578,7 +578,9 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
metadata = {'display_name': group.name}
modulestore.create_item(
user_id,
dest_usage_key,
self.location.course_key,
dest_usage_key.block_type,
block_id=dest_usage_key.block_id,
definition_data=None,
metadata=metadata,
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