Merge pull request #4381 from edx/nimisha/split-converge-api
Nimisha/split converge api
This commit is contained in:
@@ -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
|
||||
@@ -366,6 +367,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):
|
||||
"""
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
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.
|
||||
|
||||
:param course_or_parent_loc: Can be a CourseKey or UsageKey
|
||||
:param category (str): The block_type of the item we are creating
|
||||
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
|
||||
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))
|
||||
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
|
||||
|
||||
# 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
|
||||
Returns the newly created item.
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
|
||||
self.draft_store.publish(locations['parent'], user_id)
|
||||
self.draft_store.publish(locations['parent_sibling'], 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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user