Commit 0ded6699 by Braden MacDonald Committed by E. Kolpakov

Split mongo support for libraries and Library XBlock

parent 97d357cd
......@@ -68,7 +68,7 @@ def _strip_value(value, lookup='exact'):
class CourseKeyField(models.CharField):
description = "A SlashSeparatedCourseKey object, saved to the DB in the form of a string"
description = "A CourseKey object, saved to the DB in the form of a string"
__metaclass__ = models.SubfieldBase
......@@ -84,7 +84,7 @@ class CourseKeyField(models.CharField):
return None
if isinstance(value, basestring):
return SlashSeparatedCourseKey.from_deprecated_string(value)
return CourseKey.from_string(value)
else:
return value
......
......@@ -44,6 +44,9 @@ XMODULES = [
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor",
"lti = xmodule.lti_module:LTIDescriptor",
]
XBLOCKS = [
"library = xmodule.library_root_xblock:LibraryRoot",
]
setup(
name="XModule",
......@@ -64,7 +67,7 @@ setup(
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
entry_points={
'xblock.v1': XMODULES,
'xblock.v1': XMODULES + XBLOCKS,
'xmodule.v1': XMODULES,
'console_scripts': [
'xmodule_assets = xmodule.static_content:main',
......
"""
'library' XBlock (LibraryRoot)
"""
import logging
from .studio_editable import StudioEditableModule
from xblock.core import XBlock
from xblock.fields import Scope, String, List
from xblock.fragment import Fragment
log = logging.getLogger(__name__)
# Make '_' a no-op so we can scrape strings
_ = lambda text: text
class LibraryRoot(XBlock):
"""
The LibraryRoot is the root XBlock of a content library. All other blocks in
the library are its children. It contains metadata such as the library's
display_name.
"""
display_name = String(
help=_("Enter the name of the library as it should appear in Studio."),
default="Library",
display_name=_("Library Display Name"),
scope=Scope.settings
)
advanced_modules = List(
display_name=_("Advanced Module List"),
help=_("Enter the names of the advanced components to use in your library."),
scope=Scope.settings
)
has_children = True
has_author_view = True
def __unicode__(self):
return u"Library: {}".format(self.display_name)
def __str__(self):
return unicode(self).encode('utf-8')
def author_view(self, context):
"""
Renders the Studio preview view, which supports drag and drop.
"""
fragment = Fragment()
contents = []
for child_key in self.children: # pylint: disable=E1101
context['reorderable_items'].add(child_key)
child = self.runtime.get_block(child_key)
rendered_child = self.runtime.render_child(child, StudioEditableModule.get_preview_view_name(child), context)
fragment.add_frag_resources(rendered_child)
contents.append({
'id': unicode(child_key),
'content': rendered_child.content,
})
fragment.add_content(self.runtime.render_template("studio_render_children_view.html", {
'items': contents,
'xblock_context': context,
'can_add': True,
'can_reorder': True,
}))
return fragment
@property
def display_org_with_default(self):
"""
Org display names are not implemented. This just provides API compatibility with CourseDescriptor.
Always returns the raw 'org' field from the key.
"""
return self.scope_ids.usage_id.course_key.org
@property
def display_number_with_default(self):
"""
Display numbers are not implemented. This just provides API compatibility with CourseDescriptor.
Always returns the raw 'library' field from the key.
"""
return self.scope_ids.usage_id.course_key.library
@classmethod
def parse_xml(cls, xml_data, system, id_generator, **kwargs):
""" XML support not yet implemented. """
raise NotImplementedError
def add_xml_to_node(self, resource_fs):
""" XML support not yet implemented. """
raise NotImplementedError
......@@ -82,6 +82,7 @@ class ModuleStoreEnum(object):
"""
draft = 'draft-branch'
published = 'published-branch'
library = 'library'
class UserID(object):
"""
......
......@@ -13,6 +13,7 @@ from contracts import contract, new_contract
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, AssetKey
from opaque_keys.edx.locator import LibraryLocator
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from xmodule.assetstore import AssetMetadata
......@@ -25,6 +26,7 @@ from .split_migrator import SplitMigrator
new_contract('CourseKey', CourseKey)
new_contract('AssetKey', AssetKey)
new_contract('AssetMetadata', AssetMetadata)
new_contract('LibraryLocator', LibraryLocator)
log = logging.getLogger(__name__)
......@@ -259,6 +261,23 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
courses[course_id] = course
return courses.values()
@strip_key
def get_libraries(self, **kwargs):
"""
Returns a list containing the top level XBlock of the libraries (LibraryRoot) in this modulestore.
"""
libraries = {}
for store in self.modulestores:
if not hasattr(store, 'get_libraries'):
continue
# filter out ones which were fetched from earlier stores but locations may not be ==
for course in store.get_libraries(**kwargs):
course_id = self._clean_course_id_for_mapping(course.location)
if course_id not in libraries:
# course is indeed unique. save it in result
libraries[course_id] = course
return libraries.values()
def make_course_key(self, org, course, run):
"""
Return a valid :class:`~opaque_keys.edx.keys.CourseKey` for this modulestore
......@@ -291,6 +310,24 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return None
@strip_key
@contract(library_key='LibraryLocator')
def get_library(self, library_key, depth=0, **kwargs):
"""
returns the library block associated with the given key. If no such library exists,
it returns None
:param library_key: must be a LibraryLocator
"""
try:
store = self._verify_modulestore_support(library_key, 'get_library')
return store.get_library(library_key, depth=depth, **kwargs)
except NotImplementedError:
log.exception("Modulestore configured for %s does not have get_library method", library_key)
return None
except ItemNotFoundError:
return None
@strip_key
def has_course(self, course_id, ignore_case=False, **kwargs):
"""
returns the course_id of the course if it was found, else None
......@@ -508,6 +545,34 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return course
@strip_key
def create_library(self, org, library, user_id, fields, **kwargs):
"""
Creates and returns a new library.
Args:
org (str): the organization that owns the course
library (str): the code/number/name of the library
user_id: id of the user creating the course
fields (dict): Fields to set on the course at initialization - e.g. display_name
kwargs: Any optional arguments understood by a subset of modulestores to customize instantiation
Returns: a LibraryRoot
"""
# first make sure an existing course/lib doesn't already exist in the mapping
lib_key = LibraryLocator(org=org, library=library)
if lib_key in self.mappings:
raise DuplicateCourseError(lib_key, lib_key)
# create the library
store = self._verify_modulestore_support(None, 'create_library')
library = store.create_library(org, library, user_id, fields, **kwargs)
# add new library to the mapping
self.mappings[lib_key] = store
return library
@strip_key
def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs):
"""
See the superclass for the general documentation.
......
......@@ -33,7 +33,7 @@ from importlib import import_module
from opaque_keys.edx.keys import UsageKey, CourseKey, AssetKey
from opaque_keys.edx.locations import Location
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.locator import CourseLocator, LibraryLocator
from xblock.core import XBlock
from xblock.exceptions import InvalidScopeError
......@@ -875,6 +875,8 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
otherwise, do a case sensitive search
"""
assert(isinstance(course_key, CourseKey))
if isinstance(course_key, LibraryLocator):
return None # Libraries require split mongo
course_key = self.fill_in_run(course_key)
location = course_key.make_usage_key('course', course_key.run)
if ignore_case:
......
......@@ -4,7 +4,7 @@ from contracts import contract, new_contract
from lazy import lazy
from xblock.runtime import KvsFieldData
from xblock.fields import ScopeIds
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, DefinitionLocator
from opaque_keys.edx.locator import BlockUsageLocator, LocalId, CourseLocator, LibraryLocator, DefinitionLocator
from xmodule.mako_module import MakoDescriptorSystem
from xmodule.error_module import ErrorDescriptor
from xmodule.errortracker import exc_info_to_str
......@@ -19,6 +19,8 @@ from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
log = logging.getLogger(__name__)
new_contract('BlockUsageLocator', BlockUsageLocator)
new_contract('CourseLocator', CourseLocator)
new_contract('LibraryLocator', LibraryLocator)
new_contract('BlockKey', BlockKey)
new_contract('CourseEnvelope', CourseEnvelope)
......@@ -115,7 +117,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
self.modulestore.cache_block(course_key, version_guid, block_key, block)
return block
@contract(block_key=BlockKey, course_key=CourseLocator)
@contract(block_key=BlockKey, course_key="CourseLocator | LibraryLocator")
def get_module_data(self, block_key, course_key):
"""
Get block from module_data adding it to module_data if it's not already there but is in the structure
......@@ -178,8 +180,8 @@ class CachingDescriptorSystem(MakoDescriptorSystem, EditInfoRuntimeMixin):
if definition_id is None:
definition_id = LocalId()
block_locator = BlockUsageLocator(
course_key,
# Construct the Block Usage Locator:
block_locator = course_key.make_usage_key(
block_type=block_key.type,
block_id=block_key.id,
)
......
......@@ -9,7 +9,7 @@ from xmodule.modulestore.exceptions import InsufficientSpecificationError
from xmodule.modulestore.draft_and_published import (
ModuleStoreDraftAndPublished, DIRECT_ONLY_CATEGORIES, UnsupportedRevisionError
)
from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.locator import CourseLocator, LibraryLocator, LibraryUsageLocator
from xmodule.modulestore.split_mongo import BlockKey
from contracts import contract
......@@ -57,6 +57,10 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
course_id = self._map_revision_to_branch(course_id)
return super(DraftVersioningModuleStore, self).get_course(course_id, depth=depth, **kwargs)
def get_library(self, library_id, depth=0, **kwargs):
library_id = self._map_revision_to_branch(library_id)
return super(DraftVersioningModuleStore, self).get_library(library_id, depth=depth, **kwargs)
def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, revision=None, **kwargs):
"""
See :py:meth: xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.clone_course
......@@ -153,7 +157,9 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
Otherwise, raises a ValueError.
"""
with self.bulk_operations(location.course_key):
if revision == ModuleStoreEnum.RevisionOption.published_only:
if isinstance(location, LibraryUsageLocator):
branches_to_delete = [ModuleStoreEnum.BranchName.library] # Libraries don't yet have draft/publish support
elif 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]
......@@ -180,18 +186,25 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
"""
Maps RevisionOptions to BranchNames, inserting them into the key
"""
if isinstance(key, (LibraryLocator, LibraryUsageLocator)):
# Libraries don't yet have draft/publish support:
draft_branch = ModuleStoreEnum.BranchName.library
published_branch = ModuleStoreEnum.BranchName.library
else:
draft_branch = ModuleStoreEnum.BranchName.draft
published_branch = ModuleStoreEnum.BranchName.published
if revision == ModuleStoreEnum.RevisionOption.published_only:
return key.for_branch(ModuleStoreEnum.BranchName.published)
return key.for_branch(published_branch)
elif revision == ModuleStoreEnum.RevisionOption.draft_only:
return key.for_branch(ModuleStoreEnum.BranchName.draft)
return key.for_branch(draft_branch)
elif revision is None:
if key.branch is not None:
return key
elif self.get_branch_setting(key) == ModuleStoreEnum.Branch.draft_preferred:
return key.for_branch(ModuleStoreEnum.BranchName.draft)
return key.for_branch(draft_branch)
else:
return key.for_branch(ModuleStoreEnum.BranchName.published)
return key.for_branch(published_branch)
else:
raise UnsupportedRevisionError()
......
......@@ -78,6 +78,36 @@ class CourseFactory(XModuleFactory):
return new_course
class LibraryFactory(XModuleFactory):
"""
Factory for creating a content library
"""
org = factory.Sequence('org{}'.format)
library = factory.Sequence('lib{}'.format)
display_name = factory.Sequence('Test Library {}'.format)
# pylint: disable=unused-argument
@classmethod
def _create(cls, target_class, **kwargs):
"""
Create a library with a unique name and key.
All class attributes (from this class and base classes) are automagically
passed in via **kwargs.
"""
# some of the kwargst actual field values, so pop those off for use separately:
org = kwargs.pop('org')
library = kwargs.pop('library')
store = kwargs.pop('modulestore')
user_id = kwargs.pop('user_id', ModuleStoreEnum.UserID.test)
# Pass the metadata just as field=value pairs
kwargs.update(kwargs.pop('metadata', {}))
default_store_override = kwargs.pop('default_store', ModuleStoreEnum.Type.split)
with store.default_store(default_store_override):
new_library = store.create_library(org, library, user_id, fields=kwargs)
return new_library
class ItemFactory(XModuleFactory):
"""
Factory for XModule items.
......
# -*- coding: utf-8 -*-
"""
Basic unit tests related to content libraries.
Higher-level tests are in `cms/djangoapps/contentstore`.
"""
from bson.objectid import ObjectId
import ddt
from mock import patch
from opaque_keys.edx.locator import LibraryLocator
from xblock.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime
from xmodule.modulestore.exceptions import DuplicateCourseError
from xmodule.modulestore.tests.factories import LibraryFactory, ItemFactory, check_mongo_calls
from xmodule.modulestore.tests.utils import MixedSplitTestCase
from xmodule.x_module import AUTHOR_VIEW
@ddt.ddt
class TestLibraries(MixedSplitTestCase):
"""
Test for libraries.
Mostly tests code found throughout split mongo, but also tests library_root_xblock.py
"""
def test_create_library(self):
"""
Test that we can create a library, and see how many mongo calls it uses to do so.
Expected mongo calls, in order:
find_one({'org': '...', 'run': 'library', 'course': '...'})
insert(definition: {'block_type': 'library', 'fields': {}})
insert_structure(bulk)
insert_course_index(bulk)
get_course_index(bulk)
"""
with check_mongo_calls(2, 3):
LibraryFactory.create(modulestore=self.store)
def test_duplicate_library(self):
"""
Make sure we cannot create duplicate libraries
"""
org, lib_code = ('DuplicateX', "DUP")
LibraryFactory.create(org=org, library=lib_code, modulestore=self.store)
with self.assertRaises(DuplicateCourseError):
LibraryFactory.create(org=org, library=lib_code, modulestore=self.store)
@ddt.data(
"This is a test library!",
u"Ωμέγα Βιβλιοθήκη",
)
def test_str_repr(self, name):
"""
Test __unicode__() and __str__() methods of libraries
"""
library = LibraryFactory.create(metadata={"display_name": name}, modulestore=self.store)
self.assertIn(name, unicode(library))
if not isinstance(name, unicode):
self.assertIn(name, str(library))
def test_display_with_default_methods(self):
"""
Check that the display_x_with_default methods have been implemented, for
compatibility with courses.
"""
org = 'TestOrgX'
lib_code = 'LC101'
library = LibraryFactory.create(org=org, library=lib_code, modulestore=self.store)
self.assertEqual(library.display_org_with_default, org)
self.assertEqual(library.display_number_with_default, lib_code)
def test_block_with_children(self):
"""
Test that blocks used from a library can have children.
"""
library = LibraryFactory.create(modulestore=self.store)
# In the library, create a vertical block with a child:
vert_block = ItemFactory.create(
category="vertical",
parent_location=library.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
)
child_block = ItemFactory.create(
category="html",
parent_location=vert_block.location,
user_id=self.user_id,
publish_item=False,
metadata={"data": "Hello world", },
modulestore=self.store,
)
self.assertEqual(child_block.parent.replace(version_guid=None, branch=None), vert_block.location)
def test_update_item(self):
"""
Test that update_item works for a block in a library
"""
library = LibraryFactory.create(modulestore=self.store)
block = ItemFactory.create(
category="html",
parent_location=library.location,
user_id=self.user_id,
publish_item=False,
metadata={"data": "Hello world", },
modulestore=self.store,
)
block_key = block.location
block.data = "NEW"
old_version = self.store.get_item(block_key, remove_version=False, remove_branch=False).location.version_guid
self.store.update_item(block, self.user_id)
# Reload block from the modulestore
block = self.store.get_item(block_key)
self.assertEqual(block.data, "NEW")
self.assertEqual(block.location, block_key)
new_version = self.store.get_item(block_key, remove_version=False, remove_branch=False).location.version_guid
self.assertNotEqual(old_version, new_version)
def test_delete_item(self):
"""
Test to make sure delete_item() works on blocks in a library
"""
library = LibraryFactory.create(modulestore=self.store)
lib_key = library.location.library_key
block = ItemFactory.create(
category="html",
parent_location=library.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
)
library = self.store.get_library(lib_key)
self.assertEqual(len(library.children), 1)
self.store.delete_item(block.location, self.user_id)
library = self.store.get_library(lib_key)
self.assertEqual(len(library.children), 0)
def test_get_library_non_existent(self):
""" Test get_library() with non-existent key """
result = self.store.get_library(LibraryLocator("non", "existent"))
self.assertEqual(result, None)
def test_get_libraries(self):
""" Test get_libraries() """
libraries = [LibraryFactory.create(modulestore=self.store) for _ in range(0, 3)]
lib_dict = dict([(lib.location.library_key, lib) for lib in libraries])
lib_list = self.store.get_libraries()
self.assertEqual(len(lib_list), len(libraries))
for lib in lib_list:
self.assertIn(lib.location.library_key, lib_dict)
def test_strip(self):
"""
Test that library keys coming out of MixedModuleStore are stripped of
branch and version info by default.
"""
# Create a library
lib_key = LibraryFactory.create(modulestore=self.store).location.library_key
# Re-load the library from the modulestore, explicitly including version information:
lib = self.store.get_library(lib_key)
self.assertEqual(lib.location.version_guid, None)
self.assertEqual(lib.location.branch, None)
self.assertEqual(lib.location.library_key.version_guid, None)
self.assertEqual(lib.location.library_key.branch, None)
def test_get_lib_version(self):
"""
Test that we can get version data about a library from get_library()
"""
# Create a library
lib_key = LibraryFactory.create(modulestore=self.store).location.library_key
# Re-load the library from the modulestore, explicitly including version information:
lib = self.store.get_library(lib_key, remove_version=False, remove_branch=False)
version = lib.location.library_key.version_guid
self.assertIsInstance(version, ObjectId)
@patch('xmodule.modulestore.split_mongo.caching_descriptor_system.CachingDescriptorSystem.render', VanillaRuntime.render)
def test_library_author_view(self):
"""
Test that LibraryRoot.author_view can run and includes content from its
children.
We have to patch the runtime (module system) in order to be able to
render blocks in our test environment.
"""
library = LibraryFactory.create(modulestore=self.store)
# Add one HTML block to the library:
ItemFactory.create(
category="html",
parent_location=library.location,
user_id=self.user_id,
publish_item=False,
modulestore=self.store,
)
library = self.store.get_library(library.location.library_key)
context = {'reorderable_items': set(), }
# Patch the HTML block to always render "Hello world"
message = u"Hello world"
hello_render = lambda _, context: Fragment(message)
with patch('xmodule.html_module.HtmlDescriptor.author_view', hello_render, create=True):
result = library.render(AUTHOR_VIEW, context)
self.assertIn(message, result.content)
......@@ -39,6 +39,7 @@ from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.modulestore.search import path_to_location
from xmodule.modulestore.tests.factories import check_mongo_calls, check_exact_number_of_calls, \
mongo_uses_error_check
from xmodule.modulestore.tests.utils import create_modulestore_instance
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.tests import DATA_DIR, CourseComparisonTest
......@@ -1967,35 +1968,3 @@ class TestMixedModuleStore(CourseComparisonTest):
with self.store.branch_setting(ModuleStoreEnum.Branch.published_only, course_id):
published_vertical = self.store.get_item(vertical_loc)
self.assertEqual(draft_vertical.display_name, published_vertical.display_name)
# ============================================================================================================
# General utils for not using django settings
# ============================================================================================================
def load_function(path):
"""
Load a function by name.
path is a string of the form "path.to.module.function"
returns the imported python object `function` from `path.to.module`
"""
module_path, _, name = path.rpartition('.')
return getattr(import_module(module_path), name)
# pylint: disable=unused-argument
def create_modulestore_instance(engine, contentstore, doc_store_config, options, i18n_service=None, fs_service=None):
"""
This will return a new instance of a modulestore given an engine and options
"""
class_ = load_function(engine)
if issubclass(class_, ModuleStoreDraftAndPublished):
options['branch_setting_func'] = lambda: ModuleStoreEnum.Branch.draft_preferred
return class_(
doc_store_config=doc_store_config,
contentstore=contentstore,
**options
)
......@@ -29,6 +29,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.mongo import MongoKeyValueStore
from xmodule.modulestore.draft import DraftModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from opaque_keys.edx.locator import LibraryLocator
from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
......@@ -236,6 +237,16 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
assert_false(self.draft_store.has_course(mix_cased))
assert_false(self.draft_store.has_course(mix_cased, ignore_case=True))
def test_has_course_with_library(self):
"""
Test that has_course() returns False when called with a LibraryLocator.
This is required because MixedModuleStore will use has_course() to check
where a given library are stored.
"""
lib_key = LibraryLocator("TestOrg", "TestLib")
result = self.draft_store.has_course(lib_key)
assert_false(result)
def test_loads(self):
assert_not_none(
self.draft_store.get_item(Location('edX', 'toy', '2012_Fall', 'course', '2012_Fall'))
......
"""
Helper classes and methods for running modulestore tests without Django.
"""
from importlib import import_module
from opaque_keys.edx.keys import UsageKey
from unittest import TestCase
from xblock.fields import XBlockMixin
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.draft_and_published import ModuleStoreDraftAndPublished
from xmodule.modulestore.edit_info import EditInfoMixin
from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.tests import DATA_DIR
def load_function(path):
"""
Load a function by name.
path is a string of the form "path.to.module.function"
returns the imported python object `function` from `path.to.module`
"""
module_path, _, name = path.rpartition('.')
return getattr(import_module(module_path), name)
# pylint: disable=unused-argument
def create_modulestore_instance(engine, contentstore, doc_store_config, options, i18n_service=None, fs_service=None):
"""
This will return a new instance of a modulestore given an engine and options
"""
class_ = load_function(engine)
if issubclass(class_, ModuleStoreDraftAndPublished):
options['branch_setting_func'] = lambda: ModuleStoreEnum.Branch.draft_preferred
return class_(
doc_store_config=doc_store_config,
contentstore=contentstore,
**options
)
class LocationMixin(XBlockMixin):
"""
Adds a `location` property to an :class:`XBlock` so it is more compatible
with old-style :class:`XModule` API. This is a simplified version of
:class:`XModuleMixin`.
"""
@property
def location(self):
""" Get the UsageKey of this block. """
return self.scope_ids.usage_id
@location.setter
def location(self, value):
""" Set the UsageKey of this block. """
assert isinstance(value, UsageKey)
self.scope_ids = self.scope_ids._replace( # pylint: disable=attribute-defined-outside-init,protected-access
def_id=value,
usage_id=value,
)
class MixedSplitTestCase(TestCase):
"""
Stripped-down version of ModuleStoreTestCase that can be used without Django
(i.e. for testing in common/lib/ ). Sets up MixedModuleStore and Split.
"""
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': u'{}: {}, {}'.format(t_n, repr(d), repr(ctx))
modulestore_options = {
'default_class': 'xmodule.raw_module.RawDescriptor',
'fs_root': DATA_DIR,
'render_template': RENDER_TEMPLATE,
'xblock_mixins': (EditInfoMixin, InheritanceMixin, LocationMixin),
}
DOC_STORE_CONFIG = {
'host': MONGO_HOST,
'port': MONGO_PORT_NUM,
'db': 'test_mongo_libs',
'collection': 'modulestore',
'asset_collection': 'assetstore',
}
MIXED_OPTIONS = {
'stores': [
{
'NAME': 'split',
'ENGINE': 'xmodule.modulestore.split_mongo.split_draft.DraftVersioningModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
},
]
}
def setUp(self):
"""
Set up requirements for testing: a user ID and a modulestore
"""
super(MixedSplitTestCase, self).setUp()
self.user_id = ModuleStoreEnum.UserID.test
self.store = MixedModuleStore(
None,
create_modulestore_instance=create_modulestore_instance,
mappings={},
**self.MIXED_OPTIONS
)
self.addCleanup(self.store.close_all_connections)
self.addCleanup(self.store._drop_database) # pylint: disable=protected-access
......@@ -30,7 +30,7 @@
-e git+https://github.com/edx-solutions/django-splash.git@7579d052afcf474ece1239153cffe1c89935bc4f#egg=django-splash
-e git+https://github.com/edx/acid-block.git@df1a7f0cae46567c251d507b8c72168aed8ec042#egg=acid-xblock
-e git+https://github.com/edx/edx-ora2.git@release-2014-10-27T19.33#egg=edx-ora2
-e git+https://github.com/edx/opaque-keys.git@0.1.2#egg=opaque-keys
-e git+https://github.com/edx/opaque-keys.git@b12401384921c075e5a4ed7aedc3bea57f56ec32#egg=opaque-keys
-e git+https://github.com/edx/ease.git@97de68448e5495385ba043d3091f570a699d5b5f#egg=ease
-e git+https://github.com/edx/i18n-tools.git@56f048af9b6868613c14aeae760548834c495011#egg=i18n-tools
-e git+https://github.com/edx/edx-oauth2-provider.git@0.4.0#egg=oauth2-provider
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment