Commit d53a6669 by Nimisha Asthagiri

Merge pull request #4381 from edx/nimisha/split-converge-api

Nimisha/split converge api
parents 40625c45 483e2a6a
......@@ -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_and_save_xmodule(location, user_id)
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_and_save_xmodule(location, user.id)
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_and_save_xmodule(location, self.user.id)
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_and_save_xmodule(new_component_location, self.user.id)
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,16 +187,16 @@ 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)
updated_loc = 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',
parent_location=BlockUsageLocator.make_relative(
updated_loc, block_type='problem', block_id=sub.location.block_id
test_course.location.version_agnostic(), block_type='problem', block_id=sub.location.block_id
),
user_id='testbot', category='problem',
data="<problem></problem>"
......
......@@ -33,8 +33,14 @@ class TestOrphan(CourseTestCase):
def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime):
location = self.course.location.replace(category=category, name=name)
store = modulestore()
store.create_and_save_xmodule(
location, self.user.id, definition_data=data, metadata=metadata, runtime=runtime
store.create_item(
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_and_save_xmodule(course_id.make_usage_key('vertical', self.PRIVATE_VERTICAL), self.user.id)
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_and_save_xmodule(course_id.make_usage_key('vertical', self.PUBLISHED_VERTICAL), self.user.id)
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_and_save_xmodule(usage_key, user.id)
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_and_save_xmodule(
dest_usage_key,
created_block = store.create_child(
request.user.id,
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)})
......@@ -465,11 +462,11 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
else:
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
dest_module = store.create_and_save_xmodule(
dest_usage_key,
dest_module = store.create_item(
user.id,
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,
......@@ -531,7 +528,7 @@ def orphan_handler(request, course_key_string):
course_usage_key = CourseKey.from_string(course_key_string)
if request.method == 'GET':
if has_course_access(request.user, course_usage_key):
return JsonResponse(modulestore().get_orphans(course_usage_key))
return JsonResponse([unicode(item) for item in modulestore().get_orphans(course_usage_key)])
else:
raise PermissionDenied()
if request.method == 'DELETE':
......@@ -539,11 +536,9 @@ def orphan_handler(request, course_key_string):
store = modulestore()
items = store.get_orphans(course_usage_key)
for itemloc in items:
# get_orphans returns the deprecated string format w/o revision
usage_key = course_usage_key.make_usage_key_from_deprecated_string(itemloc)
# need to delete all versions
store.delete_item(usage_key, request.user.id, revision=ModuleStoreEnum.RevisionOption.all)
return JsonResponse({'deleted': items})
store.delete_item(itemloc, request.user.id, revision=ModuleStoreEnum.RevisionOption.all)
return JsonResponse({'deleted': [unicode(item) for item in items]})
else:
raise PermissionDenied()
......@@ -559,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_and_save_xmodule(usage_key, user.id)
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_and_save_xmodule(location, self.user.id)
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_and_save_xmodule(location, self.user.id)
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)
......
......@@ -63,7 +63,7 @@ STATICFILES_DIRS += [
MODULESTORE['default']['OPTIONS']['stores'].append(
{
'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split.SplitMongoModuleStore',
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': {
'render_template': 'edxmako.shortcuts.render_to_string',
......
......@@ -7,6 +7,7 @@ import logging
import re
import json
import datetime
from uuid import uuid4
from collections import namedtuple, defaultdict
import collections
......@@ -367,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
......@@ -560,50 +580,6 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
result[field.scope][field_name] = value
return result
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 create_and_save_xmodule(self, location, user_id, definition_data=None, metadata=None, runtime=None, fields={}):
"""
Create the new xmodule and save it.
:param location: a Location--must have a category
:param user_id: ID of the user creating and saving the xmodule
:param definition_data: can be empty. The initial definition_data for the kvs
:param metadata: can be empty, the initial metadata for the kvs
: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
"""
new_object = self.create_xmodule(location, definition_data, metadata, runtime, fields)
self.update_item(new_object, user_id, allow_not_found=True)
return new_object
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
......@@ -634,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):
"""
......
"""
This module provides an abstraction for Module Stores that support Draft and Published branches.
"""
from abc import ABCMeta, abstractmethod
from . import ModuleStoreEnum
class ModuleStoreDraftAndPublished(object):
"""
A mixin for a read-write database backend that supports two branches, Draft and Published, with
options to prefer Draft and fallback to Published.
"""
__metaclass__ = ABCMeta
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
def delete_item(self, location, user_id, revision=None, **kwargs):
raise NotImplementedError
@abstractmethod
def get_parent_location(self, location, revision=None, **kwargs):
raise NotImplementedError
@abstractmethod
def has_changes(self, usage_key):
raise NotImplementedError
@abstractmethod
def publish(self, location, user_id):
raise NotImplementedError
@abstractmethod
def unpublish(self, location, user_id):
raise NotImplementedError
@abstractmethod
def revert_to_published(self, location, user_id):
raise NotImplementedError
@abstractmethod
def compute_publish_state(self, xblock):
raise NotImplementedError
@abstractmethod
def convert_to_draft(self, location, user_id):
raise NotImplementedError
......@@ -6,26 +6,24 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule
"""
import logging
from uuid import uuid4
from contextlib import contextmanager
import itertools
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from . import ModuleStoreWriteBase
from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import CourseKey, UsageKey
from xmodule.modulestore.mongo.base import MongoModuleStore
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey
import itertools
from xmodule.modulestore.split_migrator import SplitMigrator
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
"""
......@@ -172,7 +170,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
raise Exception("Must pass in a course_key when calling get_items()")
store = self._get_modulestore_for_courseid(course_key)
return store.get_items(course_key, settings, content, **kwargs)
return store.get_items(course_key, settings=settings, content=content, **kwargs)
def get_courses(self):
'''
......@@ -272,7 +270,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
errs.update(store.get_errored_courses())
return errs
def create_course(self, org, course, run, user_id, fields=None, **kwargs):
def create_course(self, org, course, run, user_id, **kwargs):
"""
Creates and returns the course.
......@@ -286,11 +284,8 @@ class MixedModuleStore(ModuleStoreWriteBase):
Returns: a CourseDescriptor
"""
store = self._get_modulestore_for_courseid(None)
if not hasattr(store, 'create_course'):
raise NotImplementedError(u"Cannot create a course on store {}".format(store))
return store.create_course(org, course, run, user_id, fields, **kwargs)
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):
"""
......@@ -319,66 +314,60 @@ class MixedModuleStore(ModuleStoreWriteBase):
source_course_id, user_id, dest_course_id.org, dest_course_id.course, dest_course_id.run
)
def create_item(self, course_or_parent_loc, category, user_id, **kwargs):
def create_item(self, user_id, course_key, block_type, block_id=None, fields=None, **kwargs):
"""
Create and return the item. If parent_loc is a specific location v a course id,
it installs the new item as a child of the parent (if the parent_loc is a specific
xblock reference).
Creates and saves a new item in a course.
Returns the newly created item.
:param course_or_parent_loc: Can be a CourseKey or UsageKey
:param category (str): The block_type of the item we are creating
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
"""
# find the store for the course
course_id = getattr(course_or_parent_loc, 'course_key', course_or_parent_loc)
store = self._get_modulestore_for_courseid(course_id)
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)
location = kwargs.pop('location', None)
# invoke its create_item
if isinstance(store, MongoModuleStore):
block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex))
parent_loc = course_or_parent_loc if isinstance(course_or_parent_loc, UsageKey) else None
# must have a legitimate location, compute if appropriate
if location is None:
location = course_id.make_usage_key(category, block_id)
# do the actual creation
xblock = self.create_and_save_xmodule(location, user_id, **kwargs)
# don't forget to attach to parent
if parent_loc is not None and not 'detached' in xblock._class_tags:
parent = store.get_item(parent_loc)
parent.children.append(location)
store.update_item(parent, user_id)
elif isinstance(store, SplitMongoModuleStore):
if not isinstance(course_or_parent_loc, (CourseLocator, BlockUsageLocator)):
raise ValueError(u"Cannot create a child of {} in split. Wrong repr.".format(course_or_parent_loc))
# split handles all the fields in one dict not separated by scope
fields = kwargs.get('fields', {})
fields.update(kwargs.pop('metadata', {}))
fields.update(kwargs.pop('definition_data', {}))
kwargs['fields'] = fields
xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs)
else:
raise NotImplementedError(u"Cannot create an item on store %s" % store)
def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs):
"""
Creates and saves a new xblock that is a child of the specified block
return xblock
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=None):
def revert_to_published(self, location, user_id):
"""
Reverts an item to its last published version (recursively traversing all of its descendants).
If no published version exists, a VersionConflictError is thrown.
......@@ -388,8 +377,8 @@ class MixedModuleStore(ModuleStoreWriteBase):
:raises InvalidVersionError: if no published version exists for the location specified
"""
store = self._verify_modulestore_support(location, 'revert_to_published')
return store.revert_to_published(location, user_id=user_id)
store = self._verify_modulestore_support(location.course_key, 'revert_to_published')
return store.revert_to_published(location, user_id)
def close_all_connections(self):
"""
......@@ -408,7 +397,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
if hasattr(modulestore, '_drop_database'):
modulestore._drop_database() # pylint: disable=protected-access
def create_xmodule(self, location, definition_data=None, metadata=None, runtime=None, fields={}):
def create_xmodule(self, location, definition_data=None, metadata=None, runtime=None, fields={}, **kwargs):
"""
Create the new xmodule but don't save it. Returns the new module.
......@@ -418,8 +407,8 @@ 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')
return store.create_xmodule(location, definition_data, metadata, runtime, fields)
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):
"""
......@@ -463,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):
......@@ -471,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):
......@@ -481,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
......@@ -34,6 +35,7 @@ from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict
from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from opaque_keys.edx.locations import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
......@@ -335,7 +337,7 @@ def as_published(location):
return location.replace(revision=MongoRevisionKey.published)
class MongoModuleStore(ModuleStoreWriteBase):
class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
"""
A Mongodb backed ModuleStore
"""
......@@ -353,7 +355,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
:param doc_store_config: must have a host, db, and collection entries. Other common entries: port, tz_aware.
"""
super(MongoModuleStore, self).__init__(contentstore, **kwargs)
super(MongoModuleStore, self).__init__(contentstore=contentstore, **kwargs)
def do_connection(
db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs
......@@ -903,7 +905,12 @@ class MongoModuleStore(ModuleStoreWriteBase):
]))
location = course_id.make_usage_key('course', course_id.run)
course = self.create_and_save_xmodule(location, user_id, 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(
......@@ -911,16 +918,18 @@ class MongoModuleStore(ModuleStoreWriteBase):
name='overview'
)
overview_template = AboutDescriptor.get_template('overview.yaml')
self.create_and_save_xmodule(
about_location,
self.create_item(
user_id,
about_location.course_key,
about_location.block_type,
block_id=about_location.block_id,
definition_data=overview_template.get('data'),
runtime=course.system
)
return course
def create_xmodule(self, location, definition_data=None, metadata=None, runtime=None, fields={}):
def create_xmodule(self, location, definition_data=None, metadata=None, runtime=None, fields={}, **kwargs):
"""
Create the new xmodule but don't save it. Returns the new module.
......@@ -974,6 +983,52 @@ class MongoModuleStore(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
......@@ -1064,6 +1119,9 @@ class MongoModuleStore(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
......@@ -1161,7 +1219,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
def get_orphans(self, course_key):
"""
Return an array of all of the locations (deprecated string format) for orphans in the course.
Return an array of all of the locations for orphans in the course.
"""
course_key = self.fill_in_run(course_key)
detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")]
......@@ -1178,7 +1236,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
)
all_reachable = all_reachable.union(item.get('definition', {}).get('children', []))
item_locs -= all_reachable
return list(item_locs)
return [course_key.make_usage_key_from_deprecated_string(item_loc) for item_loc in item_locs]
def get_courses_for_wiki(self, wiki_slug):
"""
......
......@@ -54,7 +54,6 @@ class DraftModuleStore(MongoModuleStore):
This should be an attribute from ModuleStoreEnum.Branch
"""
super(DraftModuleStore, self).__init__(*args, **kwargs)
self.branch_setting_func = kwargs.pop('branch_setting_func', lambda: ModuleStoreEnum.Branch.published_only)
def get_item(self, usage_key, depth=0, revision=None):
"""
......@@ -287,7 +286,7 @@ class DraftModuleStore(MongoModuleStore):
else ModuleStoreEnum.RevisionOption.draft_preferred
return super(DraftModuleStore, self).get_parent_location(location, revision, **kwargs)
def create_xmodule(self, location, definition_data=None, metadata=None, runtime=None, fields={}):
def create_xmodule(self, location, definition_data=None, metadata=None, runtime=None, fields={}, **kwargs):
"""
Create the new xmodule but don't save it. Returns the new module with a draft locator if
the category allows drafts. If the category does not allow drafts, just creates a published module.
......@@ -483,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)
......@@ -527,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):
......
......@@ -84,12 +84,13 @@ class SplitMigrator(object):
# it doesn't need the parent as the first arg. That is, it translates and populates
# the 'children' field as it goes.
_new_module = self.split_modulestore.create_item(
course_version_locator, module.category, user_id,
user_id,
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
),
# TODO remove continue_version when bulk write is impl'd
continue_version=True
)
# after done w/ published items, add version for DRAFT pointing to the published structure
......@@ -130,7 +131,8 @@ class SplitMigrator(object):
else:
# only a draft version (aka, 'private').
_new_module = self.split_modulestore.create_item(
new_draft_course_loc, module.category, user_id,
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
......
......@@ -64,7 +64,9 @@ from opaque_keys.edx.locator import (
)
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError, \
DuplicateCourseError
from xmodule.modulestore import inheritance, ModuleStoreWriteBase, ModuleStoreEnum, PublishState
from xmodule.modulestore import (
inheritance, ModuleStoreWriteBase, ModuleStoreEnum
)
from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader
......@@ -265,7 +267,10 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
:param course_version_guid: if provided, clear only this entry
"""
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:
self.thread_cache.course_cache = {}
......@@ -325,17 +330,17 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
}
return envelope
def get_courses(self, branch=ModuleStoreEnum.BranchName.draft, qualifiers=None):
def get_courses(self, branch, qualifiers=None):
'''
Returns a list of course descriptors matching any given qualifiers.
qualifiers should be a dict of keywords matching the db fields or any
legal query for mongo to use against the active_versions collection.
Note, this is to find the current head of the named branch type
(e.g., ModuleStoreEnum.BranchName.draft). To get specific versions via guid use get_course.
Note, this is to find the current head of the named branch type.
To get specific versions via guid use get_course.
:param branch: the branch for which to return courses. Default value is ModuleStoreEnum.BranchName.draft.
:param branch: the branch for which to return courses.
:param qualifiers: an optional dict restricting which elements should match
'''
if qualifiers is None:
......@@ -415,20 +420,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
return self._get_block_from_structure(course_structure, usage_key.block_id) is not None
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
"""
draft = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.draft))
try:
published = self.get_item(usage_key.for_branch(ModuleStoreEnum.BranchName.published))
except ItemNotFoundError:
return True
return draft.update_version != published.update_version
def get_item(self, usage_key, depth=0):
"""
depth (int): An argument that some module stores may use to prefetch
......@@ -543,7 +534,9 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if block_data['category'] in detached_categories:
items.discard(decode_key_from_mongo(block_id))
return [
BlockUsageLocator(course_key=course_key, block_type=blocks[block_id]['category'], block_id=block_id)
BlockUsageLocator(
course_key=course_key, block_type=blocks[block_id]['category'], block_id=block_id
).version_agnostic()
for block_id in items
]
......@@ -640,7 +633,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Find the history of this block. Return as a VersionTree of each place the block changed (except
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
......@@ -650,7 +644,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
all_versions_with_block = self.db_connection.find_matching_structures(
{
'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}..}
......@@ -660,6 +654,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
block_payload = self._get_block_from_structure(version, block_id)
if version['_id'] == block_payload['edit_info']['update_version']:
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'])
else: # map previous to {update..}
result.setdefault(block_payload['edit_info']['previous_version'], set()).add(
......@@ -773,21 +768,16 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
serial += 1
return category + str(serial)
# DHM: Should I rewrite this to take a new xblock instance rather than to construct it? That is, require the
# caller to use XModuleDescriptor.load_from_json thus reducing similar code and making the object creation and
# validation behavior a responsibility of the model layer rather than the persistence layer.
def create_item(
self, course_or_parent_locator, category, user_id,
block_id=None, definition_locator=None, fields=None,
force=False, continue_version=False
self, user_id, course_key, block_type, block_id=None,
definition_locator=None, fields=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
of the course (if no parent provided). Return the resulting post saved version with populated locators.
Add a descriptor to persistence as an element
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.
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
:param course_key: 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.
raises InsufficientSpecificationError if there is no course locator.
......@@ -828,14 +818,14 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
the new version_guid from the locator in the returned object!
"""
# find course_index entry if applicable and structures entry
index_entry = self._get_index_if_valid(course_or_parent_locator, force, continue_version)
structure = self._lookup_course(course_or_parent_locator)['structure']
index_entry = self._get_index_if_valid(course_key, force, continue_version)
structure = self._lookup_course(course_key)['structure']
partitioned_fields = self.partition_fields_by_scope(category, fields)
partitioned_fields = self.partition_fields_by_scope(block_type, fields)
new_def_data = partitioned_fields.get(Scope.content, {})
# persist the definition if persisted != passed
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:
definition_locator, _ = self.update_definition_from_data(definition_locator, new_def_data, user_id)
......@@ -854,15 +844,15 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
else:
new_block_id = block_id
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, {})
if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children])
self._update_block_in_structure(new_structure, new_block_id, {
"category": category,
"category": block_type,
"definition": definition_locator.definition_id,
"fields": self._serialize_fields(category, block_fields),
"fields": self._serialize_fields(block_type, block_fields),
'edit_info': {
'edited_on': datetime.datetime.now(UTC),
'edited_by': user_id,
......@@ -871,17 +861,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
}
})
# if given parent, add new block as child and update parent's version
parent = None
if isinstance(course_or_parent_locator, BlockUsageLocator) and course_or_parent_locator.block_id is not None:
encoded_block_id = encode_key_for_mongo(course_or_parent_locator.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:
# db update
self.db_connection.update_structure(new_structure)
......@@ -893,22 +872,68 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# update the index entry if appropriate
if index_entry is not None:
if not continue_version:
self._update_head(index_entry, course_or_parent_locator.branch, new_id)
self._update_head(index_entry, course_key.branch, new_id)
item_loc = BlockUsageLocator(
course_or_parent_locator.version_agnostic(),
block_type=category,
course_key.version_agnostic(),
block_type=block_type,
block_id=new_block_id,
)
else:
item_loc = BlockUsageLocator(
CourseLocator(version_guid=new_id),
block_type=category,
block_type=block_type,
block_id=new_block_id,
)
# reconstruct the new_item from the cache
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):
"""
See :meth: `.ModuleStoreWrite.clone_course` for documentation.
......@@ -924,8 +949,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
)
def create_course(
self, org, course, run, user_id, fields=None,
master_branch=ModuleStoreEnum.BranchName.draft,
self, org, course, run, user_id, master_branch=None, fields=None,
versions_dict=None, root_category='course',
root_block_id='course', **kwargs
):
......@@ -1271,15 +1295,13 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if key not in new_keys or original_fields[key] != settings[key]:
return True
def xblock_publish(self, user_id, source_course, destination_course, subtree_list, blacklist):
def copy(self, user_id, source_course, destination_course, subtree_list=None, blacklist=None):
"""
Publishes each xblock in subtree_list and those blocks descendants excluding blacklist
Copies each xblock in subtree_list and those blocks descendants excluding blacklist
from source_course to destination_course.
To delete a block, publish its parent. You can blacklist the other sibs to keep them from
being refreshed. You can also just call delete_item on the destination.
To unpublish a block, call delete_item on the destination.
To delete a block in the destination_course, copy its parent and blacklist the other
sibs to keep them from being copies. You can also just call delete_item on the destination.
Ensures that each subtree occurs in the same place in destination as it does in source. If any
of the source's subtree parents are missing from destination, it raises ItemNotFound([parent_ids]).
......@@ -1355,10 +1377,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
self.db_connection.insert_structure(destination_structure)
self._update_head(index_entry, destination_course.branch, destination_structure['_id'])
def unpublish(self, location, user_id):
published_location = location.replace(branch=ModuleStoreEnum.BranchName.published)
self.delete_item(published_location, user_id)
def update_course_index(self, updated_index_entry):
"""
Change the given course's index entry.
......@@ -1445,18 +1463,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
# in case the course is later restored.
# super(SplitMongoModuleStore, self).delete_course(course_key, user_id)
def revert_to_published(self, location, user_id=None):
"""
Reverts an item to its last published version (recursively traversing all of its descendants).
If no published version exists, a VersionConflictError is thrown.
If a published version exists but there is no draft version of this item or any of its descendants, this
method is a no-op.
:raises InvalidVersionError: if no published version exists for the location specified
"""
raise NotImplementedError()
def inherit_settings(self, block_map, block_json, inheriting_settings=None):
"""
Updates block_json with any inheritable setting set by an ancestor and recurses to children.
......@@ -1863,47 +1869,3 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Check that the db is reachable.
"""
return {ModuleStoreEnum.Type.split: self.db_connection.heartbeat()}
def compute_publish_state(self, xblock):
"""
Returns whether this xblock is draft, public, or private.
Returns:
PublishState.draft - published exists and is different from draft
PublishState.public - published exists and is the same as draft
PublishState.private - no published version exists
"""
# TODO figure out what to say if xblock is not from the HEAD of its branch
def get_head(branch):
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)
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))
if not other:
if xblock.location.branch == ModuleStoreEnum.BranchName.draft:
return PublishState.private
else:
return PublishState.public # a bit nonsensical
elif xblock.update_version != other['edit_info']['update_version']:
return PublishState.draft
else:
return PublishState.public
def convert_to_draft(self, location, user_id):
"""
Create a copy of the source and mark its revision as draft.
:param source: the location of the source (its revision must be None)
"""
# This is a no-op in Split since a draft version of the data always remains
pass
"""
Module for the dual-branch fall-back Draft->Published Versioning ModuleStore
"""
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):
"""
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 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=master_branch, **kwargs
)
def get_courses(self):
"""
Returns all the courses on the Draft branch (which is a superset of the courses on the Published branch).
"""
return super(DraftVersioningModuleStore, self).get_courses(ModuleStoreEnum.BranchName.draft)
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]
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 _map_revision_to_branch(self, key, revision=None):
"""
Maps RevisionOptions to BranchNames, inserting them into the key
"""
if revision == ModuleStoreEnum.RevisionOption.published_only:
return key.for_branch(ModuleStoreEnum.BranchName.published)
elif revision == ModuleStoreEnum.RevisionOption.draft_only:
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):
'''
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):
"""
Checks if the given block has unpublished changes
: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))
except ItemNotFoundError:
return True
return draft.update_version != published.update_version
def publish(self, location, user_id, **kwargs):
"""
Save a current draft to the underlying modulestore.
Returns the newly published item.
"""
SplitMongoModuleStore.copy(
self,
user_id,
location.course_key.for_branch(ModuleStoreEnum.BranchName.draft),
location.course_key.for_branch(ModuleStoreEnum.BranchName.published),
[location],
)
def unpublish(self, location, user_id):
"""
Deletes the published version of the item.
Returns the newly unpublished item.
"""
self.delete_item(location, user_id, revision=ModuleStoreEnum.RevisionOption.published_only)
return self.get_item(location.for_branch(ModuleStoreEnum.BranchName.draft))
def revert_to_published(self, location, user_id):
"""
Reverts an item to its last published version (recursively traversing all of its descendants).
If no published version exists, a VersionConflictError is thrown.
If a published version exists but there is no draft version of this item or any of its descendants, this
method is a no-op.
:raises InvalidVersionError: if no published version exists for the location specified
"""
raise NotImplementedError()
def compute_publish_state(self, xblock):
"""
Returns whether this xblock is draft, public, or private.
Returns:
PublishState.draft - published exists and is different from draft
PublishState.public - published exists and is the same as draft
PublishState.private - no published version exists
"""
# TODO figure out what to say if xblock is not from the HEAD of its branch
def get_head(branch):
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 == ModuleStoreEnum.BranchName.draft:
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 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
elif xblock.update_version != other['edit_info']['update_version']:
return PublishState.draft
else:
return PublishState.public
def convert_to_draft(self, location, user_id):
"""
Create a copy of the source and mark its revision as draft.
:param source: the location of the source (its revision must be None)
"""
# This is a no-op in Split since a draft version of the data always remains
pass
......@@ -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_and_save_xmodule(location, user_id, 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
......@@ -61,7 +62,7 @@ class ItemFactory(SplitFactory):
# pylint: disable=W0613
@classmethod
def _create(cls, target_class, parent_location, category='chapter',
user_id=ModuleStoreEnum.UserID.test, block_id=None, definition_locator=None, force=False,
user_id=ModuleStoreEnum.UserID.test, definition_locator=None, force=False,
continue_version=False, **kwargs):
"""
passes *kwargs* as the new item's field values:
......@@ -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(
parent_location, category, user_id, definition_locator=definition_locator,
block_id=block_id, force=force, continue_version=continue_version, fields=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):
......
......@@ -8,7 +8,7 @@ import unittest
from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore import ModuleStoreEnum, PublishState
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.exceptions import InvalidVersionError
......@@ -67,7 +67,7 @@ class TestMixedModuleStore(unittest.TestCase):
},
{
'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split.SplitMongoModuleStore',
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
......@@ -117,26 +117,21 @@ class TestMixedModuleStore(unittest.TestCase):
"""
Create a course w/ one item in the persistence store using the given course & item location.
"""
course = self.store.create_course(course_key.org, course_key.course, course_key.run, self.user_id)
category = self.writable_chapter_location.category
block_id = self.writable_chapter_location.name
chapter = self.store.create_item(
# don't use course_location as it may not be the repr
course.location, category, self.user_id, location=self.writable_chapter_location, block_id=block_id
)
if isinstance(course.id, CourseLocator):
self.course_locations[self.MONGO_COURSEID] = course.location.version_agnostic()
self.writable_chapter_location = chapter.location.version_agnostic()
# create course
self.course = self.store.create_course(course_key.org, course_key.course, course_key.run, self.user_id)
if isinstance(self.course.id, CourseLocator):
self.course_locations[self.MONGO_COURSEID] = self.course.location.version_agnostic()
else:
self.assertEqual(course.id, course_key)
self.assertEqual(chapter.location, self.writable_chapter_location)
self.assertEqual(self.course.id, course_key)
self.course = course
# 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):
"""
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')
......@@ -150,6 +145,7 @@ class TestMixedModuleStore(unittest.TestCase):
BlockInfo('problem_x1a_1', 'problem', 'Problem_x1a_1', []),
BlockInfo('problem_x1a_2', 'problem', 'Problem_x1a_2', []),
BlockInfo('problem_x1a_3', 'problem', 'Problem_x1a_3', []),
BlockInfo('html_x1a_1', 'html', 'HTML_x1a_1', []),
]
)
]
......@@ -174,12 +170,13 @@ class TestMixedModuleStore(unittest.TestCase):
]
def create_sub_tree(parent, block_info):
block = self.store.create_item(parent.location, block_info.category, self.user_id, block_id=block_info.display_name)
block = self.store.create_child(
self.user_id, parent.location.version_agnostic(),
block_info.category, block_id=block_info.display_name
)
for tree in block_info.sub_tree:
create_sub_tree(block, tree)
# reload the block to update its children field
block = self.store.get_item(block.location)
setattr(self, block_info.field_name, block)
setattr(self, block_info.field_name, block.location.version_agnostic())
for tree in trees:
create_sub_tree(self.course, tree)
......@@ -206,7 +203,7 @@ class TestMixedModuleStore(unittest.TestCase):
# convert to CourseKeys
self.course_locations = {
course_id: SlashSeparatedCourseKey.from_deprecated_string(course_id)
course_id: CourseLocator.from_string(course_id)
for course_id in [self.MONGO_COURSEID, self.XML_COURSEID1, self.XML_COURSEID2]
}
# and then to the root UsageKey
......@@ -220,14 +217,9 @@ class TestMixedModuleStore(unittest.TestCase):
).make_usage_key('vertical', 'baz')
else:
self.fake_location = Location('foo', 'bar', 'slowly', 'vertical', 'baz')
self.writable_chapter_location = self.course_locations[self.MONGO_COURSEID].replace(
category='chapter', name='Overview'
)
self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace(
category='chapter', name='Overview'
)
self._create_course(default, self.course_locations[self.MONGO_COURSEID].course_key)
@ddt.data('draft', 'split')
......@@ -323,13 +315,14 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.get_item(self.writable_chapter_location)
# 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
self.course_locations[self.MONGO_COURSEID], 'vertical', user_id=self.user_id, block_id='private'
self.user_id, self.course_locations[self.MONGO_COURSEID],
'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
private_vert.location, 'html', user_id=self.user_id, 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)
......@@ -359,18 +352,18 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertFalse(self.store.has_item(leaf_loc))
self.assertNotIn(vert_loc, course.children)
# NAATODO enable for split after your converge merge
if default_ms == 'split':
return
# TODO can remove this once LMS-2869 is implemented
# first create a Published branch
self.store.publish(self.course_locations[self.MONGO_COURSEID], self.user_id)
# reproduce bug STUD-1965
# 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
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_vert.location, 'html', user_id=self.user_id, block_id='bug_leaf'
private_leaf = self.store.create_child(
self.user_id, private_vert.location, 'html', block_id='bug_leaf'
)
self.store.publish(private_vert.location, self.user_id)
......@@ -437,14 +430,13 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(parent, self.course_locations[self.XML_COURSEID1])
def verify_get_parent_locations_results(self, expected_results):
# expected_results should be a list of (child, parent, revision)
for test in expected_results:
for child_location, parent_location, revision in expected_results:
self.assertEqual(
test[1].location if test[1] else None,
self.store.get_parent_location(test[0].location, revision=test[2])
parent_location,
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):
self.initdb(default_ms)
self._create_block_hierarchy()
......@@ -453,30 +445,34 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(self.course.location, self.user_id)
# make drafts of verticals
self.store.convert_to_draft(self.vertical_x1a.location, self.user_id)
self.store.convert_to_draft(self.vertical_y1a.location, self.user_id)
self.store.convert_to_draft(self.vertical_x1a, self.user_id)
self.store.convert_to_draft(self.vertical_y1a, self.user_id)
# move child problem_x1a_1 to vertical_y1a
child_to_move = self.problem_x1a_1
old_parent = self.vertical_x1a
new_parent = self.vertical_y1a
old_parent.children.remove(child_to_move.location)
new_parent.children.append(child_to_move.location)
child_to_move_location = self.problem_x1a_1
new_parent_location = self.vertical_y1a
old_parent_location = self.vertical_x1a
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)
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.verify_get_parent_locations_results([
(child_to_move, new_parent, None),
(child_to_move, new_parent, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_move, old_parent, ModuleStoreEnum.RevisionOption.published_only),
(child_to_move_location, new_parent_location, None),
(child_to_move_location, new_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_move_location, old_parent_location.for_branch(ModuleStoreEnum.BranchName.published), ModuleStoreEnum.RevisionOption.published_only),
])
# publish the course again
self.store.publish(self.course.location, self.user_id)
self.verify_get_parent_locations_results([
(child_to_move, new_parent, None),
(child_to_move, new_parent, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_move, new_parent, ModuleStoreEnum.RevisionOption.published_only),
(child_to_move_location, new_parent_location, None),
(child_to_move_location, new_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_move_location, new_parent_location.for_branch(ModuleStoreEnum.BranchName.published), ModuleStoreEnum.RevisionOption.published_only),
])
@ddt.data('draft')
......@@ -488,29 +484,28 @@ class TestMixedModuleStore(unittest.TestCase):
self.store.publish(self.course.location, self.user_id)
# 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
child_to_delete = self.problem_y1a_1
old_parent = self.vertical_y1a
self.store.delete_item(child_to_delete.location, self.user_id)
child_to_delete_location = self.problem_y1a_1
old_parent_location = self.vertical_y1a
self.store.delete_item(child_to_delete_location, self.user_id)
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
(child_to_delete, old_parent, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_delete, old_parent, ModuleStoreEnum.RevisionOption.published_only),
(child_to_delete_location, old_parent_location, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_delete_location, old_parent_location, ModuleStoreEnum.RevisionOption.published_only),
])
# publish the course again
self.store.publish(self.course.location, self.user_id)
self.verify_get_parent_locations_results([
(child_to_delete, None, None),
(child_to_delete, None, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_delete, None, ModuleStoreEnum.RevisionOption.published_only),
(child_to_delete_location, None, None),
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.draft_preferred),
(child_to_delete_location, None, ModuleStoreEnum.RevisionOption.published_only),
])
@ddt.data('draft')
def test_revert_to_published_root_draft(self, default_ms):
"""
......@@ -518,22 +513,26 @@ class TestMixedModuleStore(unittest.TestCase):
"""
self.initdb(default_ms)
self._create_block_hierarchy()
vertical = self.store.get_item(self.vertical_x1a)
vertical_children_num = len(vertical.children)
self.store.publish(self.course.location, self.user_id)
# 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)
self.assertEqual(2, len(draft_parent.children))
draft_parent = self.store.get_item(self.vertical_x1a)
self.assertEqual(vertical_children_num - 1, len(draft_parent.children))
published_parent = self.store.get_item(
self.vertical_x1a.location,
self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.published_only
)
self.assertEqual(3, 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)
reverted_parent = self.store.get_item(self.vertical_x1a.location)
self.assertEqual(3, len(published_parent.children))
self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_parent = self.store.get_item(self.vertical_x1a)
self.assertEqual(vertical_children_num, len(published_parent.children))
self.assertEqual(reverted_parent, published_parent)
@ddt.data('draft')
......@@ -545,14 +544,15 @@ class TestMixedModuleStore(unittest.TestCase):
self._create_block_hierarchy()
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)
self.problem_x1a_1.display_name = "updated before calling revert"
self.store.update_item(self.problem_x1a_1, self.user_id)
self.store.revert_to_published(self.vertical_x1a.location, self.user_id)
problem.display_name = "updated before calling revert"
self.store.update_item(problem, 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)
@ddt.data('draft')
......@@ -564,9 +564,9 @@ class TestMixedModuleStore(unittest.TestCase):
self._create_block_hierarchy()
self.store.publish(self.course.location, self.user_id)
orig_vertical = self.vertical_x1a
self.store.revert_to_published(self.vertical_x1a.location, self.user_id)
reverted_vertical = self.store.get_item(self.vertical_x1a.location)
orig_vertical = self.store.get_item(self.vertical_x1a)
self.store.revert_to_published(self.vertical_x1a, self.user_id)
reverted_vertical = self.store.get_item(self.vertical_x1a)
self.assertEqual(orig_vertical, reverted_vertical)
@ddt.data('draft')
......@@ -577,7 +577,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms)
self._create_block_hierarchy()
with self.assertRaises(InvalidVersionError):
self.store.revert_to_published(self.vertical_x1a.location)
self.store.revert_to_published(self.vertical_x1a, self.user_id)
@ddt.data('draft')
def test_revert_to_published_direct_only(self, default_ms):
......@@ -586,22 +586,44 @@ class TestMixedModuleStore(unittest.TestCase):
"""
self.initdb(default_ms)
self._create_block_hierarchy()
self.store.revert_to_published(self.sequential_x1.location)
reverted_parent = self.store.get_item(self.sequential_x1.location)
self.store.revert_to_published(self.sequential_x1, self.user_id)
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)
self.assertEqual(1, len(reverted_parent.children))
@ddt.data('draft', 'split')
def test_get_orphans(self, default_ms):
self.initdb(default_ms)
# create an orphan
course_id = self.course_locations[self.MONGO_COURSEID].course_key
orphan = self.store.create_item(course_id, 'problem', self.user_id, block_id='orphan')
# create parented children
self._create_block_hierarchy()
# orphans
orphan_locations = [
course_id.make_usage_key('chapter', 'OrphanChapter'),
course_id.make_usage_key('vertical', 'OrphanVertical'),
course_id.make_usage_key('problem', 'OrphanProblem'),
course_id.make_usage_key('html', 'OrphanHTML'),
]
# detached items (not considered as orphans)
detached_locations = [
course_id.make_usage_key('static_tab', 'StaticTab'),
course_id.make_usage_key('about', 'overview'),
course_id.make_usage_key('course_info', 'updates'),
]
for location in (orphan_locations + detached_locations):
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)
if default_ms == 'split':
self.assertEqual(found_orphans, [orphan.location.version_agnostic()])
else:
self.assertEqual(found_orphans, [orphan.location.to_deprecated_string()])
self.assertEqual(set(found_orphans), set(orphan_locations))
@ddt.data('draft')
def test_create_item_from_parent_location(self, default_ms):
......@@ -610,7 +632,12 @@ class TestMixedModuleStore(unittest.TestCase):
new location for the child
"""
self.initdb(default_ms)
self.store.create_item(self.course_locations[self.MONGO_COURSEID], 'problem', self.user_id, block_id='orphan')
self.store.create_child(
self.user_id,
self.course_locations[self.MONGO_COURSEID],
'problem',
block_id='orphan'
)
orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans))
......@@ -631,6 +658,86 @@ class TestMixedModuleStore(unittest.TestCase):
self.assertEqual(len(self.store.get_courses_for_wiki('edX.simple.2012_Fall')), 0)
self.assertEqual(len(self.store.get_courses_for_wiki('no_such_wiki')), 0)
@ddt.data('draft', 'split')
def test_unpublish(self, default_ms):
"""
Test calling unpublish
"""
self.initdb(default_ms)
self._create_block_hierarchy()
# publish
self.store.publish(self.course.location, self.user_id)
published_xblock = self.store.get_item(
self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.published_only
)
self.assertIsNotNone(published_xblock)
# unpublish
self.store.unpublish(self.vertical_x1a, self.user_id)
with self.assertRaises(ItemNotFoundError):
self.store.get_item(
self.vertical_x1a,
revision=ModuleStoreEnum.RevisionOption.published_only
)
# make sure draft version still exists
draft_xblock = self.store.get_item(
self.vertical_x1a,
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.store.compute_publish_state(item),
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
......
......@@ -23,7 +23,7 @@ from xblock.plugin import Plugin
from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.mongo import MongoModuleStore, MongoKeyValueStore
from xmodule.modulestore.mongo import MongoKeyValueStore
from xmodule.modulestore.draft import DraftModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from opaque_keys.edx.keys import UsageKey
......@@ -148,7 +148,7 @@ class TestMongoModuleStore(unittest.TestCase):
assert_greater(len(ids), 12)
def test_mongo_modulestore_type(self):
store = MongoModuleStore(
store = DraftModuleStore(
None,
{'host': HOST, 'db': DB, 'collection': COLLECTION},
FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS
......@@ -390,13 +390,28 @@ class TestMongoModuleStore(unittest.TestCase):
def setup_test():
course = self.draft_store.get_course(course_key)
# can't use item factory as it depends on django settings
p1ele = self.draft_store.create_and_save_xmodule(
course.id.make_usage_key('problem', 'p1'), 99, runtime=course.runtime)
p2ele = self.draft_store.create_and_save_xmodule(
course.id.make_usage_key('problem', 'p2'), 99, runtime=course.runtime)
p1ele = self.draft_store.create_item(
99,
course_key,
'problem',
block_id='p1',
runtime=course.runtime
)
p2ele = self.draft_store.create_item(
99,
course_key,
'problem',
block_id='p2',
runtime=course.runtime
)
self.refloc = course.id.make_usage_key('ref_test', 'ref_test')
self.draft_store.create_and_save_xmodule(
self.refloc, 99, runtime=course.runtime, fields={
self.draft_store.create_item(
99,
self.refloc.course_key,
self.refloc.block_type,
block_id=self.refloc.block_id,
runtime=course.runtime,
fields={
'reference_link': p1ele.location,
'reference_list': [p1ele.location, p2ele.location],
'reference_dict': {'p1': p1ele.location, 'p2': p2ele.location},
......@@ -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
"""
course_location = Location('edx', 'direct', '2012_Fall', 'course', 'test_course')
chapter_location = Location('edx', 'direct', '2012_Fall', 'chapter', 'test_chapter')
course_location = Location('edX', 'toy', '2012_Fall', 'course', '2012_Fall')
chapter_location = Location('edX', 'toy', '2012_Fall', 'chapter', 'vertical_container')
# Create dummy direct only xblocks
self.draft_store.create_and_save_xmodule(course_location, user_id=self.dummy_user)
self.draft_store.create_and_save_xmodule(chapter_location, user_id=self.dummy_user)
self.draft_store.create_item(
self.dummy_user,
chapter_location.course_key,
chapter_location.block_type,
block_id=chapter_location.block_id
)
# Check that neither xblock has changes
self.assertFalse(self.draft_store.has_changes(course_location))
......@@ -512,10 +531,15 @@ class TestMongoModuleStore(unittest.TestCase):
"""
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
self.draft_store.create_and_save_xmodule(location, user_id=self.dummy_user)
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
self.assertTrue(self.draft_store.has_changes(location))
......@@ -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.
"""
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
parent = self.draft_store.create_and_save_xmodule(location, user_id=self.dummy_user)
parent.children += [Location('edX', 'missing', '2012_Fall', 'vertical', 'does_not_exist')]
parent = self.draft_store.create_item(
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)
# Check the parent for changes should return False and not throw an exception
......@@ -561,27 +590,39 @@ class TestMongoModuleStore(unittest.TestCase):
if user_id is None:
user_id = self.dummy_user
locations = {
'grandparent': Location('edX', 'tree', name, 'chapter', 'grandparent'),
'parent_sibling': Location('edX', 'tree', name, 'sequential', 'parent_sibling'),
'parent': Location('edX', 'tree', name, 'sequential', 'parent'),
'child_sibling': Location('edX', 'tree', name, 'vertical', 'child_sibling'),
'child': Location('edX', 'tree', name, 'vertical', 'child'),
}
org = 'edX'
course = 'tree{}'.format(name)
run = name
for key in locations:
self.draft_store.create_and_save_xmodule(locations[key], user_id=user_id)
if not self.draft_store.has_course(SlashSeparatedCourseKey(org, course, run)):
self.draft_store.create_course(org, course, run, user_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)
locations = {
'grandparent': Location(org, course, run, 'chapter', 'grandparent'),
'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'])
parent.children += [locations['child_sibling'], locations['child']]
self.draft_store.update_item(parent, user_id=user_id)
for key in locations:
self.draft_store.create_item(
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)
self.draft_store.publish(locations['parent_sibling'], user_id)
parent = self.draft_store.get_item(locations['parent'])
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
......@@ -663,10 +704,12 @@ class TestMongoModuleStore(unittest.TestCase):
# Create a new child and attach it to parent
new_child_location = Location('edX', 'tree', 'has_changes_add_remove_child', 'vertical', 'new_child')
self.draft_store.create_and_save_xmodule(new_child_location, user_id=self.dummy_user)
parent = self.draft_store.get_item(locations['parent'])
parent.children += [new_child_location]
self.draft_store.update_item(parent, user_id=self.dummy_user)
self.draft_store.create_child(
self.dummy_user,
locations['parent'],
new_child_location.block_type,
block_id=new_child_location.block_id
)
# Verify that the ancestors now have changes
self.assertTrue(self.draft_store.has_changes(locations['grandparent']))
......@@ -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).
"""
parent_location = Location('edX', 'test', 'non_direct_only_children', 'vertical', 'parent')
child_location = Location('edX', 'test', 'non_direct_only_children', 'html', 'child')
parent_location = Location('edX', 'toy', '2012_Fall', 'vertical', 'parent')
child_location = Location('edX', 'toy', '2012_Fall', 'html', 'child')
parent = self.draft_store.create_and_save_xmodule(parent_location, user_id=self.dummy_user)
child = self.draft_store.create_and_save_xmodule(child_location, user_id=self.dummy_user)
parent.children += [child_location]
self.draft_store.update_item(parent, user_id=self.dummy_user)
parent = self.draft_store.create_item(
self.dummy_user,
parent_location.course_key,
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)
# Verify that there are no changes
......@@ -757,10 +808,15 @@ class TestMongoModuleStore(unittest.TestCase):
"""
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
self.draft_store.create_and_save_xmodule(location, user_id=self.dummy_user)
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
component = self.draft_store.get_item(location)
......@@ -780,12 +836,17 @@ class TestMongoModuleStore(unittest.TestCase):
"""
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
publish_user = 456
# Create a dummy component to test against
self.draft_store.create_and_save_xmodule(location, user_id=create_user)
self.draft_store.create_item(
create_user,
location.course_key,
location.block_type,
block_id=location.block_id
)
# Store the current time, then publish
old_time = datetime.now(UTC)
......
from xmodule.modulestore.tests.test_split_w_old_mongo import SplitWMongoCourseBoostrapper
class TestOrphan(SplitWMongoCourseBoostrapper):
"""
Test the orphan finding code
"""
def _create_course(self):
"""
* some detached items
* some attached children
* some orphans
"""
super(TestOrphan, self)._create_course()
self._create_item('chapter', 'Chapter1', {}, {'display_name': 'Chapter 1'}, 'course', 'runid')
self._create_item('chapter', 'Chapter2', {}, {'display_name': 'Chapter 2'}, 'course', 'runid')
self._create_item('chapter', 'OrphanChapter', {}, {'display_name': 'Orphan Chapter'}, None, None)
self._create_item('vertical', 'Vert1', {}, {'display_name': 'Vertical 1'}, 'chapter', 'Chapter1')
self._create_item('vertical', 'OrphanVert', {}, {'display_name': 'Orphan Vertical'}, None, None)
self._create_item('html', 'Html1', "<p>Goodbye</p>", {'display_name': 'Parented Html'}, 'vertical', 'Vert1')
self._create_item('html', 'OrphanHtml', "<p>Hello</p>", {'display_name': 'Orphan html'}, None, None)
self._create_item('static_tab', 'staticuno', "<p>tab</p>", {'display_name': 'Tab uno'}, None, None)
self._create_item('about', 'overview', "<p>overview</p>", {}, None, None)
self._create_item('course_info', 'updates', "<ol><li><h2>Sep 22</h2><p>test</p></li></ol>", {}, None, None)
def test_mongo_orphan(self):
"""
Test that old mongo finds the orphans
"""
orphans = self.draft_mongo.get_orphans(self.old_course_key)
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
location = self.old_course_key.make_usage_key('chapter', 'OrphanChapter')
self.assertIn(location.to_deprecated_string(), orphans)
location = self.old_course_key.make_usage_key('vertical', 'OrphanVert')
self.assertIn(location.to_deprecated_string(), orphans)
location = self.old_course_key.make_usage_key('html', 'OrphanHtml')
self.assertIn(location.to_deprecated_string(), orphans)
def test_split_orphan(self):
"""
Test that split mongo finds the orphans
"""
orphans = self.split_mongo.get_orphans(self.split_course_key)
self.assertEqual(len(orphans), 3, "Wrong # {}".format(orphans))
location = self.split_course_key.make_usage_key('chapter', 'OrphanChapter')
self.assertIn(location, orphans)
location = self.split_course_key.make_usage_key('vertical', 'OrphanVert')
self.assertIn(location, orphans)
location = self.split_course_key.make_usage_key('html', 'OrphanHtml')
self.assertIn(location, orphans)
......@@ -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
......
"""
Test split_draft modulestore
"""
import unittest
import uuid
from xmodule.modulestore.split_mongo.split_draft import DraftVersioningModuleStore
from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin
from xmodule.modulestore.tests.test_split_modulestore import SplitModuleTest
# pylint: disable=W0613
def render_to_template_mock(*args):
pass
class TestDraftVersioningModuleStore(unittest.TestCase):
def setUp(self):
super(TestDraftVersioningModuleStore, self).setUp()
self.module_store = DraftVersioningModuleStore(
contentstore=None,
doc_store_config={
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore{0}'.format(uuid.uuid4().hex[:5]),
},
fs_root='',
default_class='xmodule.raw_module.RawDescriptor',
render_template=render_to_template_mock,
xblock_mixins=(InheritanceMixin, XModuleMixin),
)
self.addCleanup(self.module_store._drop_database)
SplitModuleTest.bootstrapDB(self.module_store)
def test_has_changes(self):
"""
Tests that has_changes() only returns true when changes are present
"""
draft_course = CourseLocator(
org='testx', course='GreekHero', run='run', branch=ModuleStoreEnum.BranchName.draft
)
head = draft_course.make_usage_key('course', 'head12345')
dummy_user = ModuleStoreEnum.UserID.test
# Not yet published, so changes are present
self.assertTrue(self.module_store.has_changes(head))
# Publish and verify that there are no unpublished changes
self.module_store.publish(head, dummy_user)
self.assertFalse(self.module_store.has_changes(head))
# Change the course, then check that there now are changes
course = self.module_store.get_item(head)
course.show_calculator = not course.show_calculator
self.module_store.update_item(course, dummy_user)
self.assertTrue(self.module_store.has_changes(head))
# Publish and verify again
self.module_store.publish(head, dummy_user)
self.assertFalse(self.module_store.has_changes(head))
......@@ -444,19 +444,19 @@ class SplitModuleTest(unittest.TestCase):
}
},
}
@staticmethod
def bootstrapDB():
def bootstrapDB(split_store):
'''
Sets up the initial data into the db
'''
split_store = modulestore()
for _course_id, course_spec in SplitModuleTest.COURSE_CONTENT.iteritems():
course = split_store.create_course(
course_spec['org'],
course_spec['course'],
course_spec['run'],
course_spec['user_id'],
master_branch=BRANCH_NAME_DRAFT,
fields=course_spec['fields'],
root_block_id=course_spec['root_block_id']
)
......@@ -494,7 +494,7 @@ class SplitModuleTest(unittest.TestCase):
block_id="head23456"
)
destination = CourseLocator(org="testx", course="wonderful", run="run", branch=BRANCH_NAME_PUBLISHED)
split_store.xblock_publish("test@edx.org", to_publish, destination, [to_publish], None)
split_store.copy("test@edx.org", to_publish, destination, [to_publish], None)
def setUp(self):
self.user_id = random.getrandbits(32)
......@@ -823,32 +823,6 @@ class SplitModuleItemTests(SplitModuleTest):
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(course.location.for_branch(BRANCH_NAME_PUBLISHED))
def test_has_changes(self):
"""
Tests that has_changes() only returns true when changes are present
"""
draft_course = CourseLocator(org='testx', course='GreekHero', run="run", branch=BRANCH_NAME_DRAFT)
published_course = CourseLocator(org='testx', course='GreekHero', run="run", branch=BRANCH_NAME_PUBLISHED)
head = draft_course.make_usage_key('course', 'head12345')
dummy_user = ModuleStoreEnum.UserID.test
# Not yet published, so changes are present
self.assertTrue(modulestore().has_changes(head))
# Publish and verify that there are no unpublished changes
modulestore().xblock_publish(dummy_user, draft_course, published_course, [head], None)
self.assertFalse(modulestore().has_changes(head))
# Change the course, then check that there now are changes
course = modulestore().get_item(head)
course.show_calculator = not course.show_calculator
modulestore().update_item(course, dummy_user)
self.assertTrue(modulestore().has_changes(head))
# Publish and verify again
modulestore().xblock_publish(dummy_user, draft_course, published_course, [head], None)
self.assertFalse(modulestore().has_changes(head))
def test_get_non_root(self):
# not a course obj
locator = BlockUsageLocator(
......@@ -1002,7 +976,7 @@ class TestItemCrud(SplitModuleTest):
def test_create_minimal_item(self):
"""
create_item(course_or_parent_locator, category, user, definition_locator=None, fields): new_desciptor
create_item(user, location, category, definition_locator=None, fields): new_desciptor
"""
# grab link to course to ensure new versioning works
locator = CourseLocator(org='testx', course='GreekHero', run="run", branch=BRANCH_NAME_DRAFT)
......@@ -1011,7 +985,7 @@ class TestItemCrud(SplitModuleTest):
# add minimal one w/o a parent
category = 'sequential'
new_module = modulestore().create_item(
locator, category, 'user123',
'user123', locator, category,
fields={'display_name': 'new sequential'}
)
# check that course version changed and course's previous is the other one
......@@ -1051,8 +1025,8 @@ class TestItemCrud(SplitModuleTest):
)
premod_course = modulestore().get_course(locator.course_key)
category = 'chapter'
new_module = modulestore().create_item(
locator, category, 'user123',
new_module = modulestore().create_child(
'user123', locator, category,
fields={'display_name': 'new chapter'},
definition_locator=original.definition_locator
)
......@@ -1080,13 +1054,13 @@ class TestItemCrud(SplitModuleTest):
)
category = 'problem'
new_payload = "<problem>empty</problem>"
new_module = modulestore().create_item(
locator, category, 'anotheruser',
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(
locator, category, 'anotheruser',
another_module = modulestore().create_child(
'anotheruser', locator, category,
fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=original.definition_locator,
)
......@@ -1112,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(
parent_locator, 'chapter', 'anotheruser',
modulestore().create_child(
'anotheruser', parent_locator, 'chapter',
block_id=chapter_locator.block_id,
fields={'display_name': 'chapter 99'},
)
......@@ -1123,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(
chapter_locator, 'problem', 'anotheruser',
modulestore().create_child(
'anotheruser', chapter_locator, 'problem',
block_id=problem_locator.block_id,
fields={'display_name': 'chapter 99', 'data': new_payload},
)
......@@ -1140,7 +1114,7 @@ class TestItemCrud(SplitModuleTest):
"""
# start transaction w/ simple creation
user = random.getrandbits(32)
new_course = modulestore().create_course('test_org', 'test_transaction', 'test_run', user)
new_course = modulestore().create_course('test_org', 'test_transaction', 'test_run', user, BRANCH_NAME_DRAFT)
new_course_locator = new_course.id
index_history_info = modulestore().get_course_history_info(new_course.location)
course_block_prev_version = new_course.previous_version
......@@ -1149,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(
new_course.location, 'chapter', user,
new_ele = modulestore().create_child(
user, new_course.location, 'chapter',
fields={'display_name': 'chapter 1'},
continue_version=True
)
......@@ -1167,40 +1141,40 @@ class TestItemCrud(SplitModuleTest):
# try to create existing item
with self.assertRaises(DuplicateItemError):
_fail = modulestore().create_item(
new_course.location, 'chapter', user,
_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(
new_course.location, 'chapter', user,
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(
new_course.location, 'chapter', user,
_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(
new_course.location, 'chapter', user,
_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(
course_module_locator, 'chapter', user,
new_ele = modulestore().create_child(
user, course_module_locator, 'chapter',
fields={'display_name': 'chapter 4'},
continue_version=True
)
......@@ -1309,13 +1283,13 @@ class TestItemCrud(SplitModuleTest):
)
category = 'problem'
new_payload = "<problem>empty</problem>"
modulestore().create_item(
locator, category, 'test_update_manifold',
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(
locator, category, 'test_update_manifold',
modulestore().create_child(
'test_update_manifold', locator, category,
fields={'display_name': 'problem 2', 'data': another_payload},
definition_locator=original.definition_locator,
)
......@@ -1382,7 +1356,7 @@ class TestItemCrud(SplitModuleTest):
"""
Create a course we can delete
"""
course = modulestore().create_course('nihilx', 'deletion', 'run', 'deleting_user')
course = modulestore().create_course('nihilx', 'deletion', 'run', 'deleting_user', BRANCH_NAME_DRAFT)
root = course.location.version_agnostic().for_branch(BRANCH_NAME_DRAFT)
for _ in range(4):
self.create_subtree_for_deletion(root, ['chapter', 'vertical', 'problem'])
......@@ -1394,7 +1368,9 @@ class TestItemCrud(SplitModuleTest):
"""
if not category_queue:
return
node = modulestore().create_item(parent.version_agnostic(), category_queue[0], 'deleting_user')
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):
self.create_subtree_for_deletion(node_loc, category_queue[1:])
......@@ -1409,7 +1385,9 @@ class TestCourseCreation(SplitModuleTest):
The simplest case but probing all expected results from it.
"""
# Oddly getting differences of 200nsec
new_course = modulestore().create_course('test_org', 'test_course', 'test_run', 'create_user')
new_course = modulestore().create_course(
'test_org', 'test_course', 'test_run', 'create_user', BRANCH_NAME_DRAFT
)
new_locator = new_course.location
# check index entry
index_info = modulestore().get_course_index_info(new_locator)
......@@ -1437,7 +1415,7 @@ class TestCourseCreation(SplitModuleTest):
original_locator = CourseLocator(org='testx', course='wonderful', run="run", branch=BRANCH_NAME_DRAFT)
original_index = modulestore().get_course_index_info(original_locator)
new_draft = modulestore().create_course(
'best', 'leech', 'leech_run', 'leech_master',
'best', 'leech', 'leech_run', 'leech_master', BRANCH_NAME_DRAFT,
versions_dict=original_index['versions'])
new_draft_locator = new_draft.location
self.assertRegexpMatches(new_draft_locator.org, 'best')
......@@ -1455,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(
new_draft.location, 'chapter', 'leech_master',
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()
......@@ -1493,7 +1471,7 @@ class TestCourseCreation(SplitModuleTest):
fields['grading_policy']['GRADE_CUTOFFS'] = {'A': .9, 'B': .8, 'C': .65}
fields['display_name'] = 'Derivative'
new_draft = modulestore().create_course(
'counter', 'leech', 'leech_run', 'leech_master',
'counter', 'leech', 'leech_run', 'leech_master', BRANCH_NAME_DRAFT,
versions_dict={BRANCH_NAME_DRAFT: original_index['versions'][BRANCH_NAME_DRAFT]},
fields=fields
)
......@@ -1540,7 +1518,7 @@ class TestCourseCreation(SplitModuleTest):
"""
user = random.getrandbits(32)
new_course = modulestore().create_course(
'test_org', 'test_transaction', 'test_run', user,
'test_org', 'test_transaction', 'test_run', user, BRANCH_NAME_DRAFT,
root_block_id='top', root_category='chapter'
)
self.assertEqual(new_course.location.block_id, 'top')
......@@ -1559,10 +1537,12 @@ class TestCourseCreation(SplitModuleTest):
Test create_course rejects duplicate id
"""
user = random.getrandbits(32)
courses = modulestore().get_courses()
courses = modulestore().get_courses(BRANCH_NAME_DRAFT)
with self.assertRaises(DuplicateCourseError):
dupe_course_key = courses[0].location.course_key
modulestore().create_course(dupe_course_key.org, dupe_course_key.course, dupe_course_key.run, user)
modulestore().create_course(
dupe_course_key.org, dupe_course_key.course, dupe_course_key.run, user, BRANCH_NAME_DRAFT
)
class TestInheritance(SplitModuleTest):
......@@ -1609,14 +1589,14 @@ class TestPublish(SplitModuleTest):
chapter1 = source_course.make_usage_key('chapter', 'chapter1')
chapter2 = source_course.make_usage_key('chapter', 'chapter2')
chapter3 = source_course.make_usage_key('chapter', 'chapter3')
modulestore().xblock_publish(self.user_id, source_course, dest_course, [head], [chapter2, chapter3])
modulestore().copy(self.user_id, source_course, dest_course, [head], [chapter2, chapter3])
expected = [head.block_id, chapter1.block_id]
self._check_course(
source_course, dest_course, expected, [chapter2.block_id, chapter3.block_id, "problem1", "problem3_2"]
)
# add a child under chapter1
new_module = modulestore().create_item(
chapter1, "sequential", self.user_id,
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
......@@ -1625,7 +1605,7 @@ class TestPublish(SplitModuleTest):
with self.assertRaises(ItemNotFoundError):
modulestore().get_item(new_module.location.map_into_course(dest_course))
# publish it
modulestore().xblock_publish(self.user_id, source_course, dest_course, [new_module.location], None)
modulestore().copy(self.user_id, source_course, dest_course, [new_module.location], None)
expected.append(new_module.location.block_id)
# check that it is in the published course and that its parent is the chapter
pub_module = modulestore().get_item(new_module.location.map_into_course(dest_course))
......@@ -1634,10 +1614,10 @@ class TestPublish(SplitModuleTest):
)
# ensure intentionally orphaned blocks work (e.g., course_info)
new_module = modulestore().create_item(
source_course, "course_info", self.user_id, block_id="handouts"
self.user_id, source_course, "course_info", block_id="handouts"
)
# publish it
modulestore().xblock_publish(self.user_id, source_course, dest_course, [new_module.location], None)
modulestore().copy(self.user_id, source_course, dest_course, [new_module.location], None)
expected.append(new_module.location.block_id)
# check that it is in the published course (no error means it worked)
pub_module = modulestore().get_item(new_module.location.map_into_course(dest_course))
......@@ -1656,15 +1636,15 @@ class TestPublish(SplitModuleTest):
chapter3 = source_course.make_usage_key('chapter', 'chapter3')
problem1 = source_course.make_usage_key('problem', 'problem1')
with self.assertRaises(ItemNotFoundError):
modulestore().xblock_publish(self.user_id, source_course, destination_course, [chapter3], None)
modulestore().copy(self.user_id, source_course, destination_course, [chapter3], None)
# publishing into a new branch w/o publishing the root
destination_course = CourseLocator(org='testx', course='GreekHero', run='run', branch=BRANCH_NAME_PUBLISHED)
with self.assertRaises(ItemNotFoundError):
modulestore().xblock_publish(self.user_id, source_course, destination_course, [chapter3], None)
modulestore().copy(self.user_id, source_course, destination_course, [chapter3], None)
# publishing a subdag w/o the parent already in course
modulestore().xblock_publish(self.user_id, source_course, destination_course, [head], [chapter3])
modulestore().copy(self.user_id, source_course, destination_course, [head], [chapter3])
with self.assertRaises(ItemNotFoundError):
modulestore().xblock_publish(self.user_id, source_course, destination_course, [problem1], [])
modulestore().copy(self.user_id, source_course, destination_course, [problem1], [])
def test_move_delete(self):
"""
......@@ -1675,7 +1655,7 @@ class TestPublish(SplitModuleTest):
head = source_course.make_usage_key('course', "head12345")
chapter2 = source_course.make_usage_key('chapter', 'chapter2')
problem1 = source_course.make_usage_key('problem', 'problem1')
modulestore().xblock_publish(self.user_id, source_course, dest_course, [head], [chapter2])
modulestore().copy(self.user_id, source_course, dest_course, [head], [chapter2])
expected = ["head12345", "chapter1", "chapter3", "problem1", "problem3_2"]
self._check_course(source_course, dest_course, expected, ["chapter2"])
# now move problem1 and delete problem3_2
......@@ -1684,7 +1664,7 @@ class TestPublish(SplitModuleTest):
chapter1.children.append(problem1)
chapter3.children.remove(problem1.map_into_course(chapter3.location.course_key))
modulestore().delete_item(source_course.make_usage_key("problem", "problem3_2"), self.user_id)
modulestore().xblock_publish(self.user_id, source_course, dest_course, [head], [chapter2])
modulestore().copy(self.user_id, source_course, dest_course, [head], [chapter2])
expected = ["head12345", "chapter1", "chapter3", "problem1"]
self._check_course(source_course, dest_course, expected, ["chapter2", "problem3_2"])
......@@ -1776,7 +1756,7 @@ def modulestore():
**options
)
SplitModuleTest.bootstrapDB()
SplitModuleTest.bootstrapDB(SplitModuleTest.modulestore)
return SplitModuleTest.modulestore
......
......@@ -86,9 +86,14 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
existing draft for both the new item and the parent
"""
location = self.old_course_key.make_usage_key(category, name)
self.draft_mongo.create_and_save_xmodule(
location, self.user_id, definition_data=data, metadata=metadata, runtime=self.runtime
self.draft_mongo.create_item(
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)
......@@ -105,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):
"""
......
......@@ -7,6 +7,7 @@ from xblock.fields import String, Scope, ScopeIds
from xblock.runtime import Runtime, KvsFieldData, DictKeyValueStore
from xmodule.x_module import XModuleMixin
from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.xml_importer import _import_module_and_update_references
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......@@ -39,7 +40,7 @@ class ModuleStoreNoSettings(unittest.TestCase):
'collection': COLLECTION,
}
MODULESTORE = {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
}
......@@ -85,6 +86,7 @@ def modulestore():
ModuleStoreNoSettings.modulestore = class_(
None, # contentstore
ModuleStoreNoSettings.MODULESTORE['DOC_STORE_CONFIG'],
branch_setting_func = lambda: ModuleStoreEnum.Branch.draft_preferred,
**options
)
......
......@@ -569,16 +569,18 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
This appends the new vertical to the end of children, and updates group_id_to_child.
A mutable modulestore is needed to call this method (will need to update after mixed
modulestore work, currently relies on mongo's create_and_save_xmodule method).
modulestore work, currently relies on mongo's create_item method).
"""
assert hasattr(self.system, 'modulestore') and hasattr(self.system.modulestore, 'create_and_save_xmodule'), \
assert hasattr(self.system, 'modulestore') and hasattr(self.system.modulestore, 'create_item'), \
"editor_saved should only be called when a mutable modulestore is available"
modulestore = self.system.modulestore
dest_usage_key = self.location.replace(category="vertical", name=uuid4().hex)
metadata = {'display_name': group.name}
modulestore.create_and_save_xmodule(
dest_usage_key,
modulestore.create_item(
user_id,
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