Commit 7efd0cb4 by Nimisha Asthagiri

converge Split APIs with Mixed and Draft

parent 1c595998
...@@ -36,7 +36,7 @@ def get_course_updates(location, provided_id, user_id): ...@@ -36,7 +36,7 @@ def get_course_updates(location, provided_id, user_id):
try: try:
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
course_updates = modulestore().create_and_save_xmodule(location, user_id) course_updates = modulestore().create_item(user_id, location)
course_update_items = get_course_update_items(course_updates, provided_id) course_update_items = get_course_update_items(course_updates, provided_id)
return _get_visible_update(course_update_items) return _get_visible_update(course_update_items)
...@@ -51,7 +51,7 @@ def update_course_updates(location, update, passed_id=None, user=None): ...@@ -51,7 +51,7 @@ def update_course_updates(location, update, passed_id=None, user=None):
try: try:
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
except ItemNotFoundError: except ItemNotFoundError:
course_updates = modulestore().create_and_save_xmodule(location, user.id) course_updates = modulestore().create_item(user.id, location)
course_update_items = list(reversed(get_course_update_items(course_updates))) course_update_items = list(reversed(get_course_update_items(course_updates)))
......
...@@ -575,7 +575,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase): ...@@ -575,7 +575,7 @@ class ContentStoreToyCourseTest(ContentStoreTestCase):
location = course.id.make_usage_key('chapter', 'neuvo') location = course.id.make_usage_key('chapter', 'neuvo')
# Ensure draft mongo store does not create drafts for things that shouldn't be draft # Ensure draft mongo store does not create drafts for things that shouldn't be draft
newobject = draft_store.create_and_save_xmodule(location, self.user.id) newobject = draft_store.create_item(self.user.id, location)
self.assertFalse(getattr(newobject, 'is_draft', False)) self.assertFalse(getattr(newobject, 'is_draft', False))
with self.assertRaises(InvalidVersionError): with self.assertRaises(InvalidVersionError):
draft_store.convert_to_draft(location, self.user.id) draft_store.convert_to_draft(location, self.user.id)
...@@ -1395,7 +1395,7 @@ class ContentStoreTest(ContentStoreTestCase): ...@@ -1395,7 +1395,7 @@ class ContentStoreTest(ContentStoreTestCase):
new_component_location = course.id.make_usage_key('discussion', 'new_component') new_component_location = course.id.make_usage_key('discussion', 'new_component')
# crate a new module and add it as a child to a vertical # crate a new module and add it as a child to a vertical
self.store.create_and_save_xmodule(new_component_location, self.user.id) self.store.create_item(self.user.id, new_component_location)
new_discussion_item = self.store.get_item(new_component_location) new_discussion_item = self.store.get_item(new_component_location)
......
...@@ -191,12 +191,12 @@ class TemplateTests(unittest.TestCase): ...@@ -191,12 +191,12 @@ class TemplateTests(unittest.TestCase):
self.assertIsNotNone(updated_problem.previous_version) self.assertIsNotNone(updated_problem.previous_version)
self.assertEqual(updated_problem.previous_version, first_problem.update_version) self.assertEqual(updated_problem.previous_version, first_problem.update_version)
self.assertNotEqual(updated_problem.update_version, first_problem.update_version) self.assertNotEqual(updated_problem.update_version, first_problem.update_version)
updated_loc = self.split_store.delete_item(updated_problem.location, ModuleStoreEnum.UserID.test, 'testbot') self.split_store.delete_item(updated_problem.location, ModuleStoreEnum.UserID.test, 'testbot')
second_problem = persistent_factories.ItemFactory.create( second_problem = persistent_factories.ItemFactory.create(
display_name='problem 2', display_name='problem 2',
parent_location=BlockUsageLocator.make_relative( 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', user_id='testbot', category='problem',
data="<problem></problem>" data="<problem></problem>"
......
...@@ -33,8 +33,8 @@ class TestOrphan(CourseTestCase): ...@@ -33,8 +33,8 @@ class TestOrphan(CourseTestCase):
def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime): def _create_item(self, category, name, data, metadata, parent_category, parent_name, runtime):
location = self.course.location.replace(category=category, name=name) location = self.course.location.replace(category=category, name=name)
store = modulestore() store = modulestore()
store.create_and_save_xmodule( store.create_item(
location, self.user.id, definition_data=data, metadata=metadata, runtime=runtime self.user.id, location, definition_data=data, metadata=metadata, runtime=runtime
) )
if parent_name: if parent_name:
# add child to parent in mongo # add child to parent in mongo
......
...@@ -151,11 +151,11 @@ class CourseTestCase(ModuleStoreTestCase): ...@@ -151,11 +151,11 @@ class CourseTestCase(ModuleStoreTestCase):
self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft) self.assertEqual(self.store.compute_publish_state(draft_vertical), PublishState.draft)
# create a Private (draft only) vertical # create a Private (draft only) vertical
private_vertical = self.store.create_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.make_usage_key('vertical', self.PRIVATE_VERTICAL))
self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private) self.assertEqual(self.store.compute_publish_state(private_vertical), PublishState.private)
# create a Published (no draft) vertical # create a Published (no draft) vertical
public_vertical = self.store.create_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.make_usage_key('vertical', self.PUBLISHED_VERTICAL))
public_vertical = self.store.publish(public_vertical.location, self.user.id) public_vertical = self.store.publish(public_vertical.location, self.user.id)
self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public) self.assertEqual(self.store.compute_publish_state(public_vertical), PublishState.public)
......
...@@ -287,7 +287,7 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout ...@@ -287,7 +287,7 @@ def _save_item(user, usage_key, data=None, children=None, metadata=None, nullout
if usage_key.category in CREATE_IF_NOT_FOUND: if usage_key.category in CREATE_IF_NOT_FOUND:
# New module at this location, for pages that are not pre-created. # New module at this location, for pages that are not pre-created.
# Used for course info handouts. # Used for course info handouts.
existing_item = store.create_and_save_xmodule(usage_key, user.id) existing_item = store.create_item(user.id, usage_key)
else: else:
raise raise
except InvalidLocationError: except InvalidLocationError:
...@@ -416,9 +416,9 @@ def _create_item(request): ...@@ -416,9 +416,9 @@ def _create_item(request):
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
created_block = store.create_and_save_xmodule( created_block = store.create_item(
dest_usage_key,
request.user.id, request.user.id,
dest_usage_key,
definition_data=data, definition_data=data,
metadata=metadata, metadata=metadata,
runtime=parent.runtime, runtime=parent.runtime,
...@@ -465,11 +465,9 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ ...@@ -465,11 +465,9 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_
else: else:
duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name) duplicate_metadata['display_name'] = _("Duplicate of '{0}'").format(source_item.display_name)
dest_module = store.create_and_save_xmodule( dest_module = store.create_item(
dest_usage_key,
user.id, user.id,
dest_usage_key,
definition_data=source_item.get_explicitly_set_fields_by_scope(Scope.content), definition_data=source_item.get_explicitly_set_fields_by_scope(Scope.content),
metadata=duplicate_metadata, metadata=duplicate_metadata,
runtime=source_item.runtime, runtime=source_item.runtime,
...@@ -539,10 +537,8 @@ def orphan_handler(request, course_key_string): ...@@ -539,10 +537,8 @@ def orphan_handler(request, course_key_string):
store = modulestore() store = modulestore()
items = store.get_orphans(course_usage_key) items = store.get_orphans(course_usage_key)
for itemloc in items: 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 # need to delete all versions
store.delete_item(usage_key, request.user.id, revision=ModuleStoreEnum.RevisionOption.all) store.delete_item(itemloc, request.user.id, revision=ModuleStoreEnum.RevisionOption.all)
return JsonResponse({'deleted': items}) return JsonResponse({'deleted': items})
else: else:
raise PermissionDenied() raise PermissionDenied()
...@@ -559,7 +555,7 @@ def _get_module_info(usage_key, user, rewrite_static_links=True): ...@@ -559,7 +555,7 @@ def _get_module_info(usage_key, user, rewrite_static_links=True):
except ItemNotFoundError: except ItemNotFoundError:
if usage_key.category in CREATE_IF_NOT_FOUND: if usage_key.category in CREATE_IF_NOT_FOUND:
# Create a new one for certain categories only. Used for course info handouts. # Create a new one for certain categories only. Used for course info handouts.
module = store.create_and_save_xmodule(usage_key, user.id) module = store.create_item(user.id, usage_key)
else: else:
raise raise
......
...@@ -130,7 +130,7 @@ class CourseUpdateTest(CourseTestCase): ...@@ -130,7 +130,7 @@ class CourseUpdateTest(CourseTestCase):
''' '''
# get the updates and populate 'data' field with some data. # get the updates and populate 'data' field with some data.
location = self.course.id.make_usage_key('course_info', 'updates') location = self.course.id.make_usage_key('course_info', 'updates')
course_updates = modulestore().create_and_save_xmodule(location, self.user.id) course_updates = modulestore().create_item(self.user.id, location)
update_date = u"January 23, 2014" update_date = u"January 23, 2014"
update_content = u"Hello world!" update_content = u"Hello world!"
update_data = u"<ol><li><h2>" + update_date + "</h2>" + update_content + "</li></ol>" update_data = u"<ol><li><h2>" + update_date + "</h2>" + update_content + "</li></ol>"
...@@ -204,7 +204,7 @@ class CourseUpdateTest(CourseTestCase): ...@@ -204,7 +204,7 @@ class CourseUpdateTest(CourseTestCase):
'''Test trying to add to a saved course_update which is not an ol.''' '''Test trying to add to a saved course_update which is not an ol.'''
# get the updates and set to something wrong # get the updates and set to something wrong
location = self.course.id.make_usage_key('course_info', 'updates') location = self.course.id.make_usage_key('course_info', 'updates')
modulestore().create_and_save_xmodule(location, self.user.id) modulestore().create_item(self.user.id, location)
course_updates = modulestore().get_item(location) course_updates = modulestore().get_item(location)
course_updates.data = 'bad news' course_updates.data = 'bad news'
modulestore().update_item(course_updates, self.user.id) modulestore().update_item(course_updates, self.user.id)
......
...@@ -63,7 +63,7 @@ STATICFILES_DIRS += [ ...@@ -63,7 +63,7 @@ STATICFILES_DIRS += [
MODULESTORE['default']['OPTIONS']['stores'].append( MODULESTORE['default']['OPTIONS']['stores'].append(
{ {
'NAME': 'split', 'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split.SplitMongoModuleStore', 'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG, 'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': { 'OPTIONS': {
'render_template': 'edxmako.shortcuts.render_to_string', 'render_template': 'edxmako.shortcuts.render_to_string',
......
...@@ -7,6 +7,7 @@ import logging ...@@ -7,6 +7,7 @@ import logging
import re import re
import json import json
import datetime import datetime
from uuid import uuid4
from collections import namedtuple, defaultdict from collections import namedtuple, defaultdict
import collections import collections
...@@ -560,6 +561,25 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -560,6 +561,25 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
result[field.scope][field_name] = value result[field.scope][field_name] = value
return result return result
def create_item(self, user_id, location, parent_location=None, category=None, **kwargs):
"""
Creates and saves a new item.
Either location or (category, parent_location) or both must be provided.
If parent_location is provided, a new item of the given category is added as a child.
If location is not provided, a new item with the given category and given block_id
is added to the parent_location. If the block_id is not provided, a unique name
is automatically generated.
Returns the newly created item.
:param user_id: ID of the user creating and saving the xmodule
:param location: a Location--must have a category
:param parent_location: optional parameter, specifying the Location of the parent item
:param category: optional parameter for the category of the new item
:param block_id: a unique identifier for the new item
"""
raise NotImplementedError
def update_item(self, xblock, user_id, allow_not_found=False, force=False): 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 Update the given xblock's persisted repr. Pass the user's unique id which the persistent store
...@@ -589,21 +609,6 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -589,21 +609,6 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
""" """
raise NotImplementedError 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): def clone_course(self, source_course_id, dest_course_id, user_id):
""" """
This base method just copies the assets. The lower level impls must do the actual cloning of This base method just copies the assets. The lower level impls must do the actual cloning of
...@@ -694,3 +699,18 @@ class EdxJSONEncoder(json.JSONEncoder): ...@@ -694,3 +699,18 @@ class EdxJSONEncoder(json.JSONEncoder):
return obj.isoformat() return obj.isoformat()
else: else:
return super(EdxJSONEncoder, self).default(obj) return super(EdxJSONEncoder, self).default(obj)
def compute_location_from_args(location=None, parent_location=None, **kwargs):
"""
Returns a Location object that is generated from the given arguments, as follows:
If location is provided, returns it.
If location is not provided, returns a location with the given category and given block_id.
If the block_id is not provided, a unique name is automatically generated.
"""
if location is None:
block_id = kwargs.get('block_id', kwargs.get('name', kwargs.get('display_name', uuid4().hex)))
category = kwargs.pop('category')
course_key = getattr(parent_location, 'course_key', parent_location)
location = course_key.make_usage_key(category, Location.clean(block_id))
return location
"""
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, **kwargs):
super(ModuleStoreDraftAndPublished, self).__init__(**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,17 +6,13 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule ...@@ -6,17 +6,13 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule
""" """
import logging import logging
from uuid import uuid4
from contextlib import contextmanager from contextlib import contextmanager
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from . import ModuleStoreWriteBase from . import ModuleStoreWriteBase
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.mongo.base import MongoModuleStore
from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
import itertools import itertools
from xmodule.modulestore.split_migrator import SplitMigrator from xmodule.modulestore.split_migrator import SplitMigrator
...@@ -172,7 +168,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -172,7 +168,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
raise Exception("Must pass in a course_key when calling get_items()") raise Exception("Must pass in a course_key when calling get_items()")
store = self._get_modulestore_for_courseid(course_key) 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): def get_courses(self):
''' '''
...@@ -272,7 +268,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -272,7 +268,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
errs.update(store.get_errored_courses()) errs.update(store.get_errored_courses())
return errs 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. Creates and returns the course.
...@@ -287,10 +283,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -287,10 +283,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Returns: a CourseDescriptor Returns: a CourseDescriptor
""" """
store = self._get_modulestore_for_courseid(None) store = self._get_modulestore_for_courseid(None)
if not hasattr(store, 'create_course'): return store.create_course(org, course, run, user_id, **kwargs)
raise NotImplementedError(u"Cannot create a course on store {}".format(store))
return store.create_course(org, course, run, user_id, fields, **kwargs)
def clone_course(self, source_course_id, dest_course_id, user_id): def clone_course(self, source_course_id, dest_course_id, user_id):
""" """
...@@ -319,49 +312,26 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -319,49 +312,26 @@ class MixedModuleStore(ModuleStoreWriteBase):
source_course_id, user_id, dest_course_id.org, dest_course_id.course, dest_course_id.run source_course_id, user_id, dest_course_id.org, dest_course_id.course, dest_course_id.run
) )
def create_item(self, course_or_parent_loc, category, user_id, **kwargs): def create_item(self, user_id, location=None, parent_location=None, **kwargs):
""" """
Create and return the item. If parent_loc is a specific location v a course id, Creates and saves a new item.
it installs the new item as a child of the parent (if the parent_loc is a specific Either location or (category, parent_location) or both must be provided.
xblock reference). If parent_location is provided, a new item of the given category is added as a child.
If location is not provided, a new item with the given category and given block_id
is added to the parent_location. If the block_id is not provided, a unique name
is automatically generated.
:param course_or_parent_loc: Can be a CourseKey or UsageKey Returns the newly created item.
:param category (str): The block_type of the item we are creating
"""
# 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)
location = kwargs.pop('location', None) :param user_id: ID of the user creating and saving the xmodule
# invoke its create_item :param location: a Location--must have a category
if isinstance(store, MongoModuleStore): :param parent_location: optional parameter, specifying the Location of the parent item
block_id = kwargs.pop('block_id', getattr(location, 'name', uuid4().hex)) :param category: optional parameter for the category of the new item
parent_loc = course_or_parent_loc if isinstance(course_or_parent_loc, UsageKey) else None :param block_id: a unique identifier for the new item
# must have a legitimate location, compute if appropriate """
if location is None: location = compute_location_from_args(location, parent_location, **kwargs)
location = course_id.make_usage_key(category, block_id) modulestore = self._verify_modulestore_support(location, 'create_item')
# do the actual creation return modulestore.create_item(user_id, location, parent_location, **kwargs)
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)
return xblock
def update_item(self, xblock, user_id, allow_not_found=False): def update_item(self, xblock, user_id, allow_not_found=False):
""" """
...@@ -378,7 +348,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -378,7 +348,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
store = self._verify_modulestore_support(location, 'delete_item') store = self._verify_modulestore_support(location, 'delete_item')
store.delete_item(location, user_id=user_id, **kwargs) store.delete_item(location, user_id=user_id, **kwargs)
def revert_to_published(self, location, user_id=None): def revert_to_published(self, location, user_id):
""" """
Reverts an item to its last published version (recursively traversing all of its descendants). Reverts an item to its last published version (recursively traversing all of its descendants).
If no published version exists, a VersionConflictError is thrown. If no published version exists, a VersionConflictError is thrown.
...@@ -389,7 +359,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -389,7 +359,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
:raises InvalidVersionError: if no published version exists for the location specified :raises InvalidVersionError: if no published version exists for the location specified
""" """
store = self._verify_modulestore_support(location, 'revert_to_published') store = self._verify_modulestore_support(location, 'revert_to_published')
return store.revert_to_published(location, user_id=user_id) return store.revert_to_published(location, user_id)
def close_all_connections(self): def close_all_connections(self):
""" """
...@@ -408,7 +378,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -408,7 +378,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
if hasattr(modulestore, '_drop_database'): if hasattr(modulestore, '_drop_database'):
modulestore._drop_database() # pylint: disable=protected-access 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. Create the new xmodule but don't save it. Returns the new module.
...@@ -419,7 +389,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -419,7 +389,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
:param fields: a dictionary of field names and values for the new xmodule :param fields: a dictionary of field names and values for the new xmodule
""" """
store = self._verify_modulestore_support(location, 'create_xmodule') store = self._verify_modulestore_support(location, 'create_xmodule')
return store.create_xmodule(location, definition_data, metadata, runtime, fields) return store.create_xmodule(location, definition_data, metadata, runtime, fields, **kwargs)
def get_courses_for_wiki(self, wiki_slug): def get_courses_for_wiki(self, wiki_slug):
""" """
......
...@@ -34,6 +34,7 @@ from xblock.exceptions import InvalidScopeError ...@@ -34,6 +34,7 @@ from xblock.exceptions import InvalidScopeError
from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict from xblock.fields import Scope, ScopeIds, Reference, ReferenceList, ReferenceValueDict
from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum from xmodule.modulestore import ModuleStoreWriteBase, ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, ReferentialIntegrityError from xmodule.modulestore.exceptions import ItemNotFoundError, InvalidLocationError, ReferentialIntegrityError
from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore from xmodule.modulestore.inheritance import own_metadata, InheritanceMixin, inherit_metadata, InheritanceKeyValueStore
...@@ -335,7 +336,7 @@ def as_published(location): ...@@ -335,7 +336,7 @@ def as_published(location):
return location.replace(revision=MongoRevisionKey.published) return location.replace(revision=MongoRevisionKey.published)
class MongoModuleStore(ModuleStoreWriteBase): class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
""" """
A Mongodb backed ModuleStore A Mongodb backed ModuleStore
""" """
...@@ -353,7 +354,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -353,7 +354,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
:param doc_store_config: must have a host, db, and collection entries. Other common entries: port, tz_aware. :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( def do_connection(
db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs db, collection, host, port=27017, tz_aware=True, user=None, password=None, **kwargs
...@@ -903,7 +904,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -903,7 +904,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
])) ]))
location = course_id.make_usage_key('course', course_id.run) 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_item(user_id, location, fields=fields, **kwargs)
# clone a default 'about' overview module as well # clone a default 'about' overview module as well
about_location = location.replace( about_location = location.replace(
...@@ -911,16 +912,16 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -911,16 +912,16 @@ class MongoModuleStore(ModuleStoreWriteBase):
name='overview' name='overview'
) )
overview_template = AboutDescriptor.get_template('overview.yaml') overview_template = AboutDescriptor.get_template('overview.yaml')
self.create_and_save_xmodule( self.create_item(
about_location,
user_id, user_id,
about_location,
definition_data=overview_template.get('data'), definition_data=overview_template.get('data'),
runtime=course.system runtime=course.system
) )
return course 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. Create the new xmodule but don't save it. Returns the new module.
...@@ -1161,7 +1162,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -1161,7 +1162,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
def get_orphans(self, course_key): 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) course_key = self.fill_in_run(course_key)
detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")] detached_categories = [name for name, __ in XBlock.load_tagged_classes("detached")]
...@@ -1178,7 +1179,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -1178,7 +1179,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
) )
all_reachable = all_reachable.union(item.get('definition', {}).get('children', [])) all_reachable = all_reachable.union(item.get('definition', {}).get('children', []))
item_locs -= all_reachable 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): def get_courses_for_wiki(self, wiki_slug):
""" """
......
...@@ -11,7 +11,7 @@ import logging ...@@ -11,7 +11,7 @@ import logging
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.exceptions import InvalidVersionError from xmodule.exceptions import InvalidVersionError
from xmodule.modulestore import PublishState, ModuleStoreEnum from xmodule.modulestore import PublishState, ModuleStoreEnum, compute_location_from_args
from xmodule.modulestore.exceptions import ( from xmodule.modulestore.exceptions import (
ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError ItemNotFoundError, DuplicateItemError, InvalidBranchSetting, DuplicateCourseError
) )
...@@ -54,7 +54,6 @@ class DraftModuleStore(MongoModuleStore): ...@@ -54,7 +54,6 @@ class DraftModuleStore(MongoModuleStore):
This should be an attribute from ModuleStoreEnum.Branch This should be an attribute from ModuleStoreEnum.Branch
""" """
super(DraftModuleStore, self).__init__(*args, **kwargs) 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): def get_item(self, usage_key, depth=0, revision=None):
""" """
...@@ -287,7 +286,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -287,7 +286,7 @@ class DraftModuleStore(MongoModuleStore):
else ModuleStoreEnum.RevisionOption.draft_preferred else ModuleStoreEnum.RevisionOption.draft_preferred
return super(DraftModuleStore, self).get_parent_location(location, revision, **kwargs) 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 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. the category allows drafts. If the category does not allow drafts, just creates a published module.
...@@ -306,6 +305,35 @@ class DraftModuleStore(MongoModuleStore): ...@@ -306,6 +305,35 @@ class DraftModuleStore(MongoModuleStore):
super(DraftModuleStore, self).create_xmodule(location, definition_data, metadata, runtime, fields) super(DraftModuleStore, self).create_xmodule(location, definition_data, metadata, runtime, fields)
) )
def create_item(self, user_id, location=None, parent_location=None, **kwargs):
"""
Creates and saves a new item.
Either location or (category, parent_location) or both must be provided.
If parent_location is provided, a new item of the given category is added as a child.
If location is not provided, a new item with the given category and given block_id
is added to the parent_location. If the block_id is not provided, a unique name
is automatically generated.
Returns the newly created item.
:param user_id: ID of the user creating and saving the xmodule
:param location: a Location--must have a category
:param parent_location: optional parameter, specifying the Location of the parent item
:param category: optional parameter for the category of the new item
:param block_id: a unique identifier for the new item
"""
location = compute_location_from_args(location, parent_location, **kwargs)
xblock = self.create_xmodule(location, **kwargs)
self.update_item(xblock, user_id, allow_not_found=True)
# attach to parent if given
if parent_location is not None and not 'detached' in xblock._class_tags:
parent = self.get_item(parent_location)
parent.children.append(location)
self.update_item(parent, user_id)
return xblock
def get_items(self, course_key, settings=None, content=None, revision=None, **kwargs): def get_items(self, course_key, settings=None, content=None, revision=None, **kwargs):
""" """
Performance Note: This is generally a costly operation, but useful for wildcard searches. Performance Note: This is generally a costly operation, but useful for wildcard searches.
......
...@@ -84,12 +84,11 @@ class SplitMigrator(object): ...@@ -84,12 +84,11 @@ class SplitMigrator(object):
# it doesn't need the parent as the first arg. That is, it translates and populates # it doesn't need the parent as the first arg. That is, it translates and populates
# the 'children' field as it goes. # the 'children' field as it goes.
_new_module = self.split_modulestore.create_item( _new_module = self.split_modulestore.create_item(
course_version_locator, module.category, user_id, user.id, new_locator, parent_location=course_version_locator,
block_id=module.location.block_id, block_id=module.location.block_id,
fields=self._get_json_fields_translate_references( fields=self._get_json_fields_translate_references(
module, course_version_locator, new_course.location.block_id module, course_version_locator, new_course.location.block_id
), ),
# TODO remove continue_version when bulk write is impl'd
continue_version=True continue_version=True
) )
# after done w/ published items, add version for DRAFT pointing to the published structure # after done w/ published items, add version for DRAFT pointing to the published structure
...@@ -130,7 +129,11 @@ class SplitMigrator(object): ...@@ -130,7 +129,11 @@ class SplitMigrator(object):
else: else:
# only a draft version (aka, 'private'). # only a draft version (aka, 'private').
_new_module = self.split_modulestore.create_item( _new_module = self.split_modulestore.create_item(
<<<<<<< HEAD
new_draft_course_loc, module.category, user_id, new_draft_course_loc, module.category, user_id,
=======
user.id, new_locator, parent_location=new_draft_course_loc,
>>>>>>> converge create_item.
block_id=new_locator.block_id, block_id=new_locator.block_id,
fields=self._get_json_fields_translate_references( fields=self._get_json_fields_translate_references(
module, new_draft_course_loc, published_course_usage_key.block_id module, new_draft_course_loc, published_course_usage_key.block_id
......
"""
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
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 __init__(self, **kwargs):
super(DraftVersioningModuleStore, self).__init__(**kwargs)
def create_course(self, org, course, run, user_id, **kwargs):
master_branch = kwargs.pop('master_branch', ModuleStoreEnum.BranchName.draft)
return super(DraftVersioningModuleStore, self).create_course(
org, course, run, user_id, master_branch, **kwargs
)
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.
"""
if revision == ModuleStoreEnum.RevisionOption.published_only:
branches_to_delete = [ModuleStoreEnum.BranchName.published]
elif revision == ModuleStoreEnum.RevisionOption.all:
branches_to_delete = [ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft]
else:
branches_to_delete = [ModuleStoreEnum.BranchName.draft]
for branch in branches_to_delete:
SplitMongoModuleStore.delete_item(self, location.for_branch(branch), user_id, **kwargs)
def get_parent_location(self, location, revision=None, **kwargs):
# NAATODO - support draft_preferred
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
"""
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.for_branch(ModuleStoreEnum.BranchName.published), user_id)
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 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
...@@ -174,7 +174,7 @@ class ItemFactory(XModuleFactory): ...@@ -174,7 +174,7 @@ class ItemFactory(XModuleFactory):
if display_name is not None: if display_name is not None:
metadata['display_name'] = display_name metadata['display_name'] = display_name
runtime = parent.runtime if parent else None runtime = parent.runtime if parent else None
store.create_and_save_xmodule(location, user_id, metadata=metadata, definition_data=data, runtime=runtime) store.create_item(user_id, location, metadata=metadata, definition_data=data, runtime=runtime)
module = store.get_item(location) module = store.get_item(location)
......
...@@ -61,7 +61,7 @@ class ItemFactory(SplitFactory): ...@@ -61,7 +61,7 @@ class ItemFactory(SplitFactory):
# pylint: disable=W0613 # pylint: disable=W0613
@classmethod @classmethod
def _create(cls, target_class, parent_location, category='chapter', 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): continue_version=False, **kwargs):
""" """
passes *kwargs* as the new item's field values: passes *kwargs* as the new item's field values:
...@@ -74,8 +74,8 @@ class ItemFactory(SplitFactory): ...@@ -74,8 +74,8 @@ class ItemFactory(SplitFactory):
""" """
modulestore = kwargs.pop('modulestore') modulestore = kwargs.pop('modulestore')
return modulestore.create_item( return modulestore.create_item(
parent_location, category, user_id, definition_locator=definition_locator, user_id, category=category, parent_location=parent_location, defintion_locator=definition_locator,
block_id=block_id, force=force, continue_version=continue_version, fields=kwargs force=force, continue_version=continue_version, **kwargs
) )
@classmethod @classmethod
......
...@@ -67,7 +67,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -67,7 +67,7 @@ class TestMixedModuleStore(unittest.TestCase):
}, },
{ {
'NAME': 'split', 'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split.SplitMongoModuleStore', 'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG, 'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
}, },
...@@ -118,11 +118,11 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -118,11 +118,11 @@ class TestMixedModuleStore(unittest.TestCase):
Create a course w/ one item in the persistence store using the given course & item location. Create a course w/ one item in the persistence store using the given course & item location.
""" """
course = self.store.create_course(course_key.org, course_key.course, course_key.run, self.user_id) 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 block_id = self.writable_chapter_location.name
chapter = self.store.create_item( chapter = self.store.create_item(
# don't use course_location as it may not be the repr # 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 self.user_id, self.writable_chapter_location,
parent_location=course.location, block_id=block_id
) )
if isinstance(course.id, CourseLocator): if isinstance(course.id, CourseLocator):
self.course_locations[self.MONGO_COURSEID] = course.location.version_agnostic() self.course_locations[self.MONGO_COURSEID] = course.location.version_agnostic()
...@@ -174,7 +174,10 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -174,7 +174,10 @@ class TestMixedModuleStore(unittest.TestCase):
] ]
def create_sub_tree(parent, block_info): 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_item(
self.user_id, parent_location=parent.location,
category=block_info.category, block_id=block_info.display_name
)
for tree in block_info.sub_tree: for tree in block_info.sub_tree:
create_sub_tree(block, tree) create_sub_tree(block, tree)
# reload the block to update its children field # reload the block to update its children field
...@@ -226,8 +229,6 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -226,8 +229,6 @@ class TestMixedModuleStore(unittest.TestCase):
self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace( self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace(
category='chapter', name='Overview' category='chapter', name='Overview'
) )
self._create_course(default, self.course_locations[self.MONGO_COURSEID].course_key) self._create_course(default, self.course_locations[self.MONGO_COURSEID].course_key)
@ddt.data('draft', 'split') @ddt.data('draft', 'split')
...@@ -325,11 +326,12 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -325,11 +326,12 @@ class TestMixedModuleStore(unittest.TestCase):
# create and delete a private vertical with private children # create and delete a private vertical with private children
private_vert = self.store.create_item( private_vert = self.store.create_item(
# don't use course_location as it may not be the repr # don't use course_location as it may not be the repr
self.course_locations[self.MONGO_COURSEID], 'vertical', user_id=self.user_id, block_id='private' self.user_id, parent_location=self.course_locations[self.MONGO_COURSEID],
category='vertical', block_id='private'
) )
private_leaf = self.store.create_item( private_leaf = self.store.create_item(
# don't use course_location as it may not be the repr # 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, parent_location=private_vert.location, category='html', block_id='private_leaf'
) )
# verify pre delete state (just to verify that the test is valid) # verify pre delete state (just to verify that the test is valid)
...@@ -577,7 +579,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -577,7 +579,7 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
with self.assertRaises(InvalidVersionError): with self.assertRaises(InvalidVersionError):
self.store.revert_to_published(self.vertical_x1a.location) self.store.revert_to_published(self.vertical_x1a.location, self.user_id)
@ddt.data('draft') @ddt.data('draft')
def test_revert_to_published_direct_only(self, default_ms): def test_revert_to_published_direct_only(self, default_ms):
...@@ -586,7 +588,7 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -586,7 +588,7 @@ class TestMixedModuleStore(unittest.TestCase):
""" """
self.initdb(default_ms) self.initdb(default_ms)
self._create_block_hierarchy() self._create_block_hierarchy()
self.store.revert_to_published(self.sequential_x1.location) self.store.revert_to_published(self.sequential_x1.location, self.user_id)
reverted_parent = self.store.get_item(self.sequential_x1.location) reverted_parent = self.store.get_item(self.sequential_x1.location)
# It does not discard the child vertical, even though that child is a draft (with no published version) # It does not discard the child vertical, even though that child is a draft (with no published version)
self.assertEqual(1, len(reverted_parent.children)) self.assertEqual(1, len(reverted_parent.children))
...@@ -596,12 +598,10 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -596,12 +598,10 @@ class TestMixedModuleStore(unittest.TestCase):
self.initdb(default_ms) self.initdb(default_ms)
# create an orphan # create an orphan
course_id = self.course_locations[self.MONGO_COURSEID].course_key course_id = self.course_locations[self.MONGO_COURSEID].course_key
orphan = self.store.create_item(course_id, 'problem', self.user_id, block_id='orphan') orphan_location = course_id.make_usage_key('problem', 'orphan')
orphan = self.store.create_item(self.user_id, orphan_location)
found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key) found_orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
if default_ms == 'split': self.assertEqual(found_orphans, [orphan_location])
self.assertEqual(found_orphans, [orphan.location.version_agnostic()])
else:
self.assertEqual(found_orphans, [orphan.location.to_deprecated_string()])
@ddt.data('draft') @ddt.data('draft')
def test_create_item_from_parent_location(self, default_ms): def test_create_item_from_parent_location(self, default_ms):
...@@ -610,7 +610,10 @@ class TestMixedModuleStore(unittest.TestCase): ...@@ -610,7 +610,10 @@ class TestMixedModuleStore(unittest.TestCase):
new location for the child new location for the child
""" """
self.initdb(default_ms) self.initdb(default_ms)
self.store.create_item(self.course_locations[self.MONGO_COURSEID], 'problem', self.user_id, block_id='orphan') self.store.create_item(
self.user_id, parent_location=self.course_locations[self.MONGO_COURSEID],
category='problem', block_id='orphan'
)
orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key) orphans = self.store.get_orphans(self.course_locations[self.MONGO_COURSEID].course_key)
self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans)) self.assertEqual(len(orphans), 0, "unexpected orphans: {}".format(orphans))
......
...@@ -23,7 +23,7 @@ from xblock.plugin import Plugin ...@@ -23,7 +23,7 @@ from xblock.plugin import Plugin
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum 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 xmodule.modulestore.draft import DraftModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.keys import UsageKey
...@@ -148,7 +148,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -148,7 +148,7 @@ class TestMongoModuleStore(unittest.TestCase):
assert_greater(len(ids), 12) assert_greater(len(ids), 12)
def test_mongo_modulestore_type(self): def test_mongo_modulestore_type(self):
store = MongoModuleStore( store = DraftModuleStore(
None, None,
{'host': HOST, 'db': DB, 'collection': COLLECTION}, {'host': HOST, 'db': DB, 'collection': COLLECTION},
FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS
...@@ -390,13 +390,13 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -390,13 +390,13 @@ class TestMongoModuleStore(unittest.TestCase):
def setup_test(): def setup_test():
course = self.draft_store.get_course(course_key) course = self.draft_store.get_course(course_key)
# can't use item factory as it depends on django settings # can't use item factory as it depends on django settings
p1ele = self.draft_store.create_and_save_xmodule( p1ele = self.draft_store.create_item(
course.id.make_usage_key('problem', 'p1'), 99, runtime=course.runtime) 99, course.id.make_usage_key('problem', 'p1'), runtime=course.runtime)
p2ele = self.draft_store.create_and_save_xmodule( p2ele = self.draft_store.create_item(
course.id.make_usage_key('problem', 'p2'), 99, runtime=course.runtime) 99, course.id.make_usage_key('problem', 'p2'), runtime=course.runtime)
self.refloc = course.id.make_usage_key('ref_test', 'ref_test') self.refloc = course.id.make_usage_key('ref_test', 'ref_test')
self.draft_store.create_and_save_xmodule( self.draft_store.create_item(
self.refloc, 99, runtime=course.runtime, fields={ 99,self.refloc, runtime=course.runtime, fields={
'reference_link': p1ele.location, 'reference_link': p1ele.location,
'reference_list': [p1ele.location, p2ele.location], 'reference_list': [p1ele.location, p2ele.location],
'reference_dict': {'p1': p1ele.location, 'p2': p2ele.location}, 'reference_dict': {'p1': p1ele.location, 'p2': p2ele.location},
...@@ -501,8 +501,8 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -501,8 +501,8 @@ class TestMongoModuleStore(unittest.TestCase):
chapter_location = Location('edx', 'direct', '2012_Fall', 'chapter', 'test_chapter') chapter_location = Location('edx', 'direct', '2012_Fall', 'chapter', 'test_chapter')
# Create dummy direct only xblocks # Create dummy direct only xblocks
self.draft_store.create_and_save_xmodule(course_location, user_id=self.dummy_user) self.draft_store.create_item(self.dummy_user, course_location)
self.draft_store.create_and_save_xmodule(chapter_location, user_id=self.dummy_user) self.draft_store.create_item(self.dummy_user, chapter_location)
# Check that neither xblock has changes # Check that neither xblock has changes
self.assertFalse(self.draft_store.has_changes(course_location)) self.assertFalse(self.draft_store.has_changes(course_location))
...@@ -515,7 +515,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -515,7 +515,7 @@ class TestMongoModuleStore(unittest.TestCase):
location = Location('edX', 'changes', '2012_Fall', 'vertical', 'test_vertical') location = Location('edX', 'changes', '2012_Fall', 'vertical', 'test_vertical')
# Create a dummy component to test against # 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)
# Not yet published, so changes are present # Not yet published, so changes are present
self.assertTrue(self.draft_store.has_changes(location)) self.assertTrue(self.draft_store.has_changes(location))
...@@ -570,7 +570,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -570,7 +570,7 @@ class TestMongoModuleStore(unittest.TestCase):
} }
for key in locations: for key in locations:
self.draft_store.create_and_save_xmodule(locations[key], user_id=user_id) self.draft_store.create_item(user_id, locations[key])
grandparent = self.draft_store.get_item(locations['grandparent']) grandparent = self.draft_store.get_item(locations['grandparent'])
grandparent.children += [locations['parent_sibling'], locations['parent']] grandparent.children += [locations['parent_sibling'], locations['parent']]
...@@ -663,7 +663,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -663,7 +663,7 @@ class TestMongoModuleStore(unittest.TestCase):
# Create a new child and attach it to parent # Create a new child and attach it to parent
new_child_location = Location('edX', 'tree', 'has_changes_add_remove_child', 'vertical', 'new_child') new_child_location = Location('edX', 'tree', 'has_changes_add_remove_child', 'vertical', 'new_child')
self.draft_store.create_and_save_xmodule(new_child_location, user_id=self.dummy_user) self.draft_store.create_item(self.dummy_user, new_child_location)
parent = self.draft_store.get_item(locations['parent']) parent = self.draft_store.get_item(locations['parent'])
parent.children += [new_child_location] parent.children += [new_child_location]
self.draft_store.update_item(parent, user_id=self.dummy_user) self.draft_store.update_item(parent, user_id=self.dummy_user)
...@@ -688,8 +688,8 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -688,8 +688,8 @@ class TestMongoModuleStore(unittest.TestCase):
parent_location = Location('edX', 'test', 'non_direct_only_children', 'vertical', 'parent') parent_location = Location('edX', 'test', 'non_direct_only_children', 'vertical', 'parent')
child_location = Location('edX', 'test', 'non_direct_only_children', 'html', 'child') child_location = Location('edX', 'test', 'non_direct_only_children', 'html', 'child')
parent = self.draft_store.create_and_save_xmodule(parent_location, user_id=self.dummy_user) parent = self.draft_store.create_item(self.dummy_user, parent_location)
child = self.draft_store.create_and_save_xmodule(child_location, user_id=self.dummy_user) child = self.draft_store.create_item(self.dummy_user, child_location)
parent.children += [child_location] parent.children += [child_location]
self.draft_store.update_item(parent, user_id=self.dummy_user) self.draft_store.update_item(parent, user_id=self.dummy_user)
self.draft_store.publish(parent_location, self.dummy_user) self.draft_store.publish(parent_location, self.dummy_user)
...@@ -760,7 +760,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -760,7 +760,7 @@ class TestMongoModuleStore(unittest.TestCase):
location = Location('edX', 'editInfoTest', '2012_Fall', 'html', 'test_html') location = Location('edX', 'editInfoTest', '2012_Fall', 'html', 'test_html')
# Create a dummy component to test against # 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)
# Store the current edit time and verify that dummy_user created the component # Store the current edit time and verify that dummy_user created the component
component = self.draft_store.get_item(location) component = self.draft_store.get_item(location)
...@@ -785,7 +785,7 @@ class TestMongoModuleStore(unittest.TestCase): ...@@ -785,7 +785,7 @@ class TestMongoModuleStore(unittest.TestCase):
publish_user = 456 publish_user = 456
# Create a dummy component to test against # 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)
# Store the current time, then publish # Store the current time, then publish
old_time = datetime.now(UTC) old_time = datetime.now(UTC)
......
"""
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),
)
# NAATODO - uncomment once merged with drop_database PR
# self.addCleanup(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))
...@@ -86,7 +86,6 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase): ...@@ -86,7 +86,6 @@ class SplitWMongoCourseBoostrapper(unittest.TestCase):
existing draft for both the new item and the parent existing draft for both the new item and the parent
""" """
location = self.old_course_key.make_usage_key(category, name) location = self.old_course_key.make_usage_key(category, name)
self.draft_mongo.create_and_save_xmodule( self.draft_mongo.create_and_save_xmodule(
location, self.user_id, definition_data=data, metadata=metadata, runtime=self.runtime location, self.user_id, definition_data=data, metadata=metadata, runtime=self.runtime
) )
......
...@@ -7,6 +7,7 @@ from xblock.fields import String, Scope, ScopeIds ...@@ -7,6 +7,7 @@ from xblock.fields import String, Scope, ScopeIds
from xblock.runtime import Runtime, KvsFieldData, DictKeyValueStore from xblock.runtime import Runtime, KvsFieldData, DictKeyValueStore
from xmodule.x_module import XModuleMixin from xmodule.x_module import XModuleMixin
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.xml_importer import _import_module_and_update_references from xmodule.modulestore.xml_importer import _import_module_and_update_references
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
...@@ -39,7 +40,7 @@ class ModuleStoreNoSettings(unittest.TestCase): ...@@ -39,7 +40,7 @@ class ModuleStoreNoSettings(unittest.TestCase):
'collection': COLLECTION, 'collection': COLLECTION,
} }
MODULESTORE = { MODULESTORE = {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.DraftMongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG, 'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options 'OPTIONS': modulestore_options
} }
...@@ -85,6 +86,7 @@ def modulestore(): ...@@ -85,6 +86,7 @@ def modulestore():
ModuleStoreNoSettings.modulestore = class_( ModuleStoreNoSettings.modulestore = class_(
None, # contentstore None, # contentstore
ModuleStoreNoSettings.MODULESTORE['DOC_STORE_CONFIG'], ModuleStoreNoSettings.MODULESTORE['DOC_STORE_CONFIG'],
branch_setting_func = lambda: ModuleStoreEnum.Branch.draft_preferred,
**options **options
) )
......
...@@ -571,14 +571,14 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes ...@@ -571,14 +571,14 @@ class SplitTestDescriptor(SplitTestFields, SequenceDescriptor, StudioEditableDes
A mutable modulestore is needed to call this method (will need to update after mixed 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_and_save_xmodule 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" "editor_saved should only be called when a mutable modulestore is available"
modulestore = self.system.modulestore modulestore = self.system.modulestore
dest_usage_key = self.location.replace(category="vertical", name=uuid4().hex) dest_usage_key = self.location.replace(category="vertical", name=uuid4().hex)
metadata = {'display_name': group.name} metadata = {'display_name': group.name}
modulestore.create_and_save_xmodule( modulestore.create_item(
dest_usage_key,
user_id, user_id,
dest_usage_key,
definition_data=None, definition_data=None,
metadata=metadata, metadata=metadata,
runtime=self.system, runtime=self.system,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment