Commit b4478081 by Don Mitchell

Merge pull request #2738 from edx/dbarch/mixed_ms_router

Dbarch/mixed ms router
parents a5d9ed5b e915a32a
...@@ -1003,7 +1003,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1003,7 +1003,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case. # We had a bug where orphaned draft nodes caused export to fail. This is here to cover that case.
vertical.location = mongo.draft.as_draft(vertical.location.replace(name='no_references')) vertical.location = mongo.draft.as_draft(vertical.location.replace(name='no_references'))
draft_store.save_xmodule(vertical) draft_store.update_item(vertical, allow_not_found=True)
orphan_vertical = draft_store.get_item(vertical.location) orphan_vertical = draft_store.get_item(vertical.location)
self.assertEqual(orphan_vertical.location.name, 'no_references') self.assertEqual(orphan_vertical.location.name, 'no_references')
...@@ -1020,13 +1020,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase): ...@@ -1020,13 +1020,14 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
# now create a new/different private (draft only) vertical # now create a new/different private (draft only) vertical
vertical.location = mongo.draft.as_draft(Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None])) vertical.location = mongo.draft.as_draft(Location(['i4x', 'edX', 'toy', 'vertical', 'a_private_vertical', None]))
draft_store.save_xmodule(vertical) draft_store.update_item(vertical, allow_not_found=True)
private_vertical = draft_store.get_item(vertical.location) private_vertical = draft_store.get_item(vertical.location)
vertical = None # blank out b/c i destructively manipulated its location 2 lines above vertical = None # blank out b/c i destructively manipulated its location 2 lines above
# add the new private to list of children # add the new private to list of children
sequential = module_store.get_item(Location(['i4x', 'edX', 'toy', sequential = module_store.get_item(
'sequential', 'vertical_sequential', None])) Location('i4x', 'edX', 'toy', 'sequential', 'vertical_sequential', None)
)
private_location_no_draft = private_vertical.location.replace(revision=None) private_location_no_draft = private_vertical.location.replace(revision=None)
sequential.children.append(private_location_no_draft.url()) sequential.children.append(private_location_no_draft.url())
module_store.update_item(sequential, self.user.id) module_store.update_item(sequential, self.user.id)
......
...@@ -17,6 +17,7 @@ from .utils import CourseTestCase ...@@ -17,6 +17,7 @@ from .utils import CourseTestCase
import contentstore.git_export_utils as git_export_utils import contentstore.git_export_utils as git_export_utils
from xmodule.contentstore.django import _CONTENTSTORE from xmodule.contentstore.django import _CONTENTSTORE
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from contentstore.utils import get_modulestore
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE) TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
...@@ -70,7 +71,7 @@ class TestExportGit(CourseTestCase): ...@@ -70,7 +71,7 @@ class TestExportGit(CourseTestCase):
Test failed course export response. Test failed course export response.
""" """
self.course_module.giturl = 'foobar' self.course_module.giturl = 'foobar'
modulestore().save_xmodule(self.course_module) get_modulestore(self.course_module.location).update_item(self.course_module)
response = self.client.get('{}?action=push'.format(self.test_url)) response = self.client.get('{}?action=push'.format(self.test_url))
self.assertIn('Export Failed:', response.content) self.assertIn('Export Failed:', response.content)
...@@ -93,7 +94,7 @@ class TestExportGit(CourseTestCase): ...@@ -93,7 +94,7 @@ class TestExportGit(CourseTestCase):
self.populateCourse() self.populateCourse()
self.course_module.giturl = 'file://{}'.format(bare_repo_dir) self.course_module.giturl = 'file://{}'.format(bare_repo_dir)
modulestore().save_xmodule(self.course_module) get_modulestore(self.course_module.location).update_item(self.course_module)
response = self.client.get('{}?action=push'.format(self.test_url)) response = self.client.get('{}?action=push'.format(self.test_url))
self.assertIn('Export Succeeded', response.content) self.assertIn('Export Succeeded', response.content)
...@@ -26,7 +26,7 @@ from xblock.runtime import Mixologist ...@@ -26,7 +26,7 @@ from xblock.runtime import Mixologist
from lms.lib.xblock.runtime import unquote_slashes from lms.lib.xblock.runtime import unquote_slashes
from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState from contentstore.utils import get_lms_link_for_item, compute_unit_state, UnitState, get_modulestore
from contentstore.views.helpers import get_parent_xblock from contentstore.views.helpers import get_parent_xblock
from models.settings.course_grading import CourseGradingModel from models.settings.course_grading import CourseGradingModel
...@@ -365,7 +365,7 @@ def component_handler(request, usage_id, handler, suffix=''): ...@@ -365,7 +365,7 @@ def component_handler(request, usage_id, handler, suffix=''):
location = unquote_slashes(usage_id) location = unquote_slashes(usage_id)
descriptor = modulestore().get_item(location) descriptor = get_modulestore(location).get_item(location)
# Let the module handle the AJAX # Let the module handle the AJAX
req = django_to_webob_request(request) req = django_to_webob_request(request)
...@@ -376,6 +376,8 @@ def component_handler(request, usage_id, handler, suffix=''): ...@@ -376,6 +376,8 @@ def component_handler(request, usage_id, handler, suffix=''):
log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True) log.info("XBlock %s attempted to access missing handler %r", descriptor, handler, exc_info=True)
raise Http404 raise Http404
modulestore().save_xmodule(descriptor) # unintentional update to handle any side effects of handle call; so, request user didn't author
# the change
get_modulestore(location).update_item(descriptor, None)
return webob_to_django_response(resp) return webob_to_django_response(resp)
...@@ -202,7 +202,9 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v ...@@ -202,7 +202,9 @@ def xblock_view_handler(request, package_id, view_name, tag=None, branch=None, v
log.debug("unable to render studio_view for %r", component, exc_info=True) log.debug("unable to render studio_view for %r", component, exc_info=True)
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)})) fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))
store.save_xmodule(component) # change not authored by requestor but by xblocks.
store.update_item(component, None)
elif view_name == 'student_view' and component.has_children: elif view_name == 'student_view' and component.has_children:
# For non-leaf xblocks on the unit page, show the special rendering # For non-leaf xblocks on the unit page, show the special rendering
# which links to the new container page. # which links to the new container page.
...@@ -519,8 +521,8 @@ def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -519,8 +521,8 @@ def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid
if request.method == 'DELETE': if request.method == 'DELETE':
if request.user.is_staff: if request.user.is_staff:
items = modulestore().get_orphans(old_location, 'draft') items = modulestore().get_orphans(old_location, 'draft')
for item in items: for itemloc in items:
modulestore('draft').delete_item(item, delete_all_versions=True) modulestore('draft').delete_item(itemloc, delete_all_versions=True)
return JsonResponse({'deleted': items}) return JsonResponse({'deleted': items})
else: else:
raise PermissionDenied() raise PermissionDenied()
......
...@@ -661,11 +661,11 @@ class TestComponentHandler(TestCase): ...@@ -661,11 +661,11 @@ class TestComponentHandler(TestCase):
def setUp(self): def setUp(self):
self.request_factory = RequestFactory() self.request_factory = RequestFactory()
patcher = patch('contentstore.views.component.modulestore') patcher = patch('contentstore.views.component.get_modulestore')
self.modulestore = patcher.start() self.get_modulestore = patcher.start()
self.addCleanup(patcher.stop) self.addCleanup(patcher.stop)
self.descriptor = self.modulestore.return_value.get_item.return_value self.descriptor = self.get_modulestore.return_value.get_item.return_value
self.usage_id = 'dummy_usage_id' self.usage_id = 'dummy_usage_id'
......
...@@ -16,7 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy' ...@@ -16,7 +16,7 @@ os.environ['SERVICE_VARIANT'] = 'bok_choy'
os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120 os.environ['CONFIG_ROOT'] = path(__file__).abspath().dirname() #pylint: disable=E1120
from .aws import * # pylint: disable=W0401, W0614 from .aws import * # pylint: disable=W0401, W0614
from xmodule.x_module import prefer_xmodules from xmodule.modulestore import prefer_xmodules
######################### Testing overrides #################################### ######################### Testing overrides ####################################
......
...@@ -34,7 +34,8 @@ from path import path ...@@ -34,7 +34,8 @@ from path import path
from lms.lib.xblock.mixin import LmsBlockMixin from lms.lib.xblock.mixin import LmsBlockMixin
from cms.lib.xblock.mixin import CmsBlockMixin from cms.lib.xblock.mixin import CmsBlockMixin
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin, only_xmodules from xmodule.modulestore import only_xmodules
from xmodule.x_module import XModuleMixin
from dealer.git import git from dealer.git import git
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -229,7 +230,7 @@ XBLOCK_SELECT_FUNCTION = only_xmodules ...@@ -229,7 +230,7 @@ XBLOCK_SELECT_FUNCTION = only_xmodules
# either by uncommenting them here, or adding them to your private.py # either by uncommenting them here, or adding them to your private.py
# You should also enable the ALLOW_ALL_ADVANCED_COMPONENTS feature flag, so that # You should also enable the ALLOW_ALL_ADVANCED_COMPONENTS feature flag, so that
# xblocks can be added via advanced settings # xblocks can be added via advanced settings
# from xmodule.x_module import prefer_xmodules # from xmodule.modulestore import prefer_xmodules
# XBLOCK_SELECT_FUNCTION = prefer_xmodules # XBLOCK_SELECT_FUNCTION = prefer_xmodules
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
......
...@@ -16,6 +16,7 @@ from .common import * ...@@ -16,6 +16,7 @@ from .common import *
import os import os
from path import path from path import path
from warnings import filterwarnings from warnings import filterwarnings
from xmodule.modulestore import prefer_xmodules
# Nose Test Runner # Nose Test Runner
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
...@@ -158,7 +159,6 @@ filterwarnings('ignore', message='No request passed to the backend, unable to ra ...@@ -158,7 +159,6 @@ filterwarnings('ignore', message='No request passed to the backend, unable to ra
################################# XBLOCK ###################################### ################################# XBLOCK ######################################
from xmodule.x_module import prefer_xmodules
XBLOCK_SELECT_FUNCTION = prefer_xmodules XBLOCK_SELECT_FUNCTION = prefer_xmodules
......
...@@ -31,3 +31,10 @@ class SerializationError(Exception): ...@@ -31,3 +31,10 @@ class SerializationError(Exception):
def __init__(self, location, msg): def __init__(self, location, msg):
super(SerializationError, self).__init__(msg) super(SerializationError, self).__init__(msg)
self.location = location self.location = location
class UndefinedContext(Exception):
"""
Tried to access an xmodule field which needs a different context (runtime) to have a value.
"""
pass
...@@ -7,11 +7,15 @@ import logging ...@@ -7,11 +7,15 @@ import logging
import re import re
from collections import namedtuple from collections import namedtuple
import collections
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from xblock.plugin import default_select
from .exceptions import InvalidLocationError, InsufficientSpecificationError from .exceptions import InvalidLocationError, InsufficientSpecificationError
from xmodule.errortracker import make_error_tracker from xmodule.errortracker import make_error_tracker
from xblock.runtime import Mixologist
from xblock.core import XBlock
log = logging.getLogger('edx.modulestore') log = logging.getLogger('edx.modulestore')
...@@ -447,7 +451,7 @@ class ModuleStoreWrite(ModuleStoreRead): ...@@ -447,7 +451,7 @@ class ModuleStoreWrite(ModuleStoreRead):
pass pass
@abstractmethod @abstractmethod
def delete_item(self, location, user_id=None, delete_all_versions=False, delete_children=False, force=False): def delete_item(self, location, user_id=None, **kwargs):
""" """
Delete an item from persistence. Pass the user's unique id which the persistent store 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. should save with the update if it has that ability.
...@@ -475,7 +479,9 @@ class ModuleStoreReadBase(ModuleStoreRead): ...@@ -475,7 +479,9 @@ class ModuleStoreReadBase(ModuleStoreRead):
metadata_inheritance_cache_subsystem=None, request_cache=None, metadata_inheritance_cache_subsystem=None, request_cache=None,
modulestore_update_signal=None, xblock_mixins=(), xblock_select=None, modulestore_update_signal=None, xblock_mixins=(), xblock_select=None,
# temporary parms to enable backward compatibility. remove once all envs migrated # temporary parms to enable backward compatibility. remove once all envs migrated
db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None db=None, collection=None, host=None, port=None, tz_aware=True, user=None, password=None,
# allow lower level init args to pass harmlessly
** kwargs
): ):
''' '''
Set up the error-tracking logic. Set up the error-tracking logic.
...@@ -529,9 +535,75 @@ class ModuleStoreReadBase(ModuleStoreRead): ...@@ -529,9 +535,75 @@ class ModuleStoreReadBase(ModuleStoreRead):
return c return c
return None return None
def update_item(self, xblock, user_id=None, 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 package_id 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=None, delete_all_versions=False, delete_children=False, 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 delete_all_versions: removes both the draft and published version of this item from
the course if using draft and old mongo. Split may or may not implement this.
: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 package_id and version_guid given and the current
version head != version_guid and force is not True. (only applicable to version tracking stores)
"""
raise NotImplementedError
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
''' '''
Implement interface functionality that can be shared. Implement interface functionality that can be shared.
''' '''
pass def __init__(self, **kwargs):
super(ModuleStoreWriteBase, self).__init__(**kwargs)
# TODO: Don't have a runtime just to generate the appropriate mixin classes (cpennington)
# This is only used by partition_fields_by_scope, which is only needed because
# the split mongo store is used for item creation as well as item persistence
self.mixologist = Mixologist(self.xblock_mixins)
def partition_fields_by_scope(self, category, fields):
"""
Return dictionary of {scope: {field1: val, ..}..} for the fields of this potential xblock
:param category: the xblock category
:param fields: the dictionary of {fieldname: value}
"""
if fields is None:
return {}
cls = self.mixologist.mix(XBlock.load_class(category, select=prefer_xmodules))
result = collections.defaultdict(dict)
for field_name, value in fields.iteritems():
field = getattr(cls, field_name)
result[field.scope][field_name] = value
return result
def only_xmodules(identifier, entry_points):
"""Only use entry_points that are supplied by the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
return default_select(identifier, from_xmodule)
def prefer_xmodules(identifier, entry_points):
"""Prefer entry_points from the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
if from_xmodule:
return default_select(identifier, from_xmodule)
else:
return default_select(identifier, entry_points)
...@@ -119,7 +119,8 @@ class LocMapperStore(object): ...@@ -119,7 +119,8 @@ class LocMapperStore(object):
return package_id return package_id
def translate_location(self, old_style_course_id, location, published=True, add_entry_if_missing=True): def translate_location(self, old_style_course_id, location, published=True,
add_entry_if_missing=True, passed_block_id=None):
""" """
Translate the given module location to a Locator. If the mapping has the run id in it, then you Translate the given module location to a Locator. If the mapping has the run id in it, then you
should provide old_style_course_id with that run id in it to disambiguate the mapping if there exists more should provide old_style_course_id with that run id in it to disambiguate the mapping if there exists more
...@@ -137,6 +138,8 @@ class LocMapperStore(object): ...@@ -137,6 +138,8 @@ class LocMapperStore(object):
:param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if :param add_entry_if_missing: a boolean as to whether to raise ItemNotFoundError or to create an entry if
the course the course
or block is not found in the map. or block is not found in the map.
:param passed_block_id: what block_id to assign and save if none is found
(only if add_entry_if_missing)
NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category NOTE: unlike old mongo, draft branches contain the whole course; so, it applies to all category
of locations including course. of locations including course.
...@@ -158,7 +161,7 @@ class LocMapperStore(object): ...@@ -158,7 +161,7 @@ class LocMapperStore(object):
self.create_map_entry(course_location) self.create_map_entry(course_location)
entry = self.location_map.find_one(location_id) entry = self.location_map.find_one(location_id)
else: else:
raise ItemNotFoundError() raise ItemNotFoundError(location)
elif len(maps) == 1: elif len(maps) == 1:
entry = maps[0] entry = maps[0]
else: else:
...@@ -172,7 +175,9 @@ class LocMapperStore(object): ...@@ -172,7 +175,9 @@ class LocMapperStore(object):
block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name)) block_id = entry['block_map'].get(self.encode_key_for_mongo(location.name))
if block_id is None: if block_id is None:
if add_entry_if_missing: if add_entry_if_missing:
block_id = self._add_to_block_map(location, location_id, entry['block_map']) block_id = self._add_to_block_map(
location, location_id, entry['block_map'], passed_block_id
)
else: else:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
elif isinstance(block_id, dict): elif isinstance(block_id, dict):
...@@ -188,7 +193,7 @@ class LocMapperStore(object): ...@@ -188,7 +193,7 @@ class LocMapperStore(object):
elif add_entry_if_missing: elif add_entry_if_missing:
block_id = self._add_to_block_map(location, location_id, entry['block_map']) block_id = self._add_to_block_map(location, location_id, entry['block_map'])
else: else:
raise ItemNotFoundError() raise ItemNotFoundError(location)
else: else:
raise InvalidLocationError() raise InvalidLocationError()
...@@ -297,7 +302,7 @@ class LocMapperStore(object): ...@@ -297,7 +302,7 @@ class LocMapperStore(object):
maps = self.location_map.find(location_id) maps = self.location_map.find(location_id)
maps = list(maps) maps = list(maps)
if len(maps) == 0: if len(maps) == 0:
raise ItemNotFoundError() raise ItemNotFoundError(location)
elif len(maps) == 1: elif len(maps) == 1:
entry = maps[0] entry = maps[0]
else: else:
...@@ -315,18 +320,19 @@ class LocMapperStore(object): ...@@ -315,18 +320,19 @@ class LocMapperStore(object):
else: else:
return draft_course_locator return draft_course_locator
def _add_to_block_map(self, location, location_id, block_map): def _add_to_block_map(self, location, location_id, block_map, block_id=None):
'''add the given location to the block_map and persist it''' '''add the given location to the block_map and persist it'''
if self._block_id_is_guid(location.name): if block_id is None:
# This makes the ids more meaningful with a small probability of name collision. if self._block_id_is_guid(location.name):
# The downside is that if there's more than one course mapped to from the same org/course root # This makes the ids more meaningful with a small probability of name collision.
# the block ids will likely be out of sync and collide from an id perspective. HOWEVER, # The downside is that if there's more than one course mapped to from the same org/course root
# if there are few == org/course roots or their content is unrelated, this will work well. # the block ids will likely be out of sync and collide from an id perspective. HOWEVER,
block_id = self._verify_uniqueness(location.category + location.name[:3], block_map) # if there are few == org/course roots or their content is unrelated, this will work well.
else: block_id = self._verify_uniqueness(location.category + location.name[:3], block_map)
# if 2 different category locations had same name, then they'll collide. Make the later else:
# mapped ones unique # if 2 different category locations had same name, then they'll collide. Make the later
block_id = self._verify_uniqueness(location.name, block_map) # mapped ones unique
block_id = self._verify_uniqueness(location.name, block_map)
encoded_location_name = self.encode_key_for_mongo(location.name) encoded_location_name = self.encode_key_for_mongo(location.name)
block_map.setdefault(encoded_location_name, {})[location.category] = block_id block_map.setdefault(encoded_location_name, {})[location.category] = block_id
self.location_map.update(location_id, {'$set': {'block_map': block_map}}) self.location_map.update(location_id, {'$set': {'block_map': block_map}})
......
...@@ -51,6 +51,12 @@ class Locator(object): ...@@ -51,6 +51,12 @@ class Locator(object):
def __eq__(self, other): def __eq__(self, other):
return self.__dict__ == other.__dict__ return self.__dict__ == other.__dict__
def __hash__(self):
"""
Hash on contents.
"""
return hash(unicode(self))
def __repr__(self): def __repr__(self):
''' '''
repr(self) returns something like this: CourseLocator("mit.eecs.6002x") repr(self) returns something like this: CourseLocator("mit.eecs.6002x")
...@@ -198,16 +204,14 @@ class CourseLocator(Locator): ...@@ -198,16 +204,14 @@ class CourseLocator(Locator):
""" """
Return a string representing this location. Return a string representing this location.
""" """
parts = []
if self.package_id: if self.package_id:
result = unicode(self.package_id) parts.append(unicode(self.package_id))
if self.branch: if self.branch:
result += '/' + BRANCH_PREFIX + self.branch parts.append(u"{prefix}{branch}".format(prefix=BRANCH_PREFIX, branch=self.branch))
return result if self.version_guid:
elif self.version_guid: parts.append(u"{prefix}{guid}".format(prefix=VERSION_PREFIX, guid=self.version_guid))
return u"{prefix}{guid}".format(prefix=VERSION_PREFIX, guid=self.version_guid) return u"/".join(parts)
else:
# raise InsufficientSpecificationError("missing package_id or version_guid")
return '<InsufficientSpecificationError: missing package_id or version_guid>'
def url(self): def url(self):
""" """
...@@ -432,22 +436,25 @@ class BlockUsageLocator(CourseLocator): ...@@ -432,22 +436,25 @@ class BlockUsageLocator(CourseLocator):
def version_agnostic(self): def version_agnostic(self):
""" """
Returns a copy of itself.
If both version_guid and package_id are known, use a blank package_id in the copy.
We don't care if the locator's version is not the current head; so, avoid version conflict We don't care if the locator's version is not the current head; so, avoid version conflict
by reducing info. by reducing info.
Returns a copy of itself without any version info.
:param block_locator: :raises: ValueError if the block locator has no package_id
""" """
if self.version_guid: return BlockUsageLocator(package_id=self.package_id,
return BlockUsageLocator(version_guid=self.version_guid, branch=self.branch,
branch=self.branch, block_id=self.block_id)
block_id=self.block_id)
else: def course_agnostic(self):
return BlockUsageLocator(package_id=self.package_id, """
branch=self.branch, We only care about the locator's version not its course.
block_id=self.block_id) Returns a copy of itself without any course info.
:raises: ValueError if the block locator has no version_guid
"""
return BlockUsageLocator(version_guid=self.version_guid,
block_id=self.block_id)
def set_block_id(self, new): def set_block_id(self, new):
""" """
......
...@@ -625,7 +625,22 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -625,7 +625,22 @@ class MongoModuleStore(ModuleStoreWriteBase):
modules = self._load_items(list(items), depth) modules = self._load_items(list(items), depth)
return modules return modules
def create_xmodule(self, location, definition_data=None, metadata=None, system=None): def create_course(self, course_id, definition_data=None, metadata=None, runtime=None):
"""
Create a course with the given course_id.
"""
if isinstance(course_id, Location):
location = course_id
if location.category != 'course':
raise ValueError(u"Course roots must be of category 'course': {}".format(unicode(location)))
else:
course_dict = Location.parse_course_id(course_id)
course_dict['category'] = 'course'
course_dict['tag'] = 'i4x'
location = Location(course_dict)
return self.create_and_save_xmodule(location, definition_data, metadata, runtime)
def create_xmodule(self, location, definition_data=None, metadata=None, system=None, fields={}):
""" """
Create the new xmodule but don't save it. Returns the new module. Create the new xmodule but don't save it. Returns the new module.
...@@ -672,36 +687,18 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -672,36 +687,18 @@ class MongoModuleStore(ModuleStoreWriteBase):
ScopeIds(None, location.category, location, location), ScopeIds(None, location.category, location, location),
dbmodel, dbmodel,
) )
for key, value in fields.iteritems():
setattr(xmodule, key, value)
# decache any pending field settings from init # decache any pending field settings from init
xmodule.save() xmodule.save()
return xmodule return xmodule
def save_xmodule(self, xmodule): def create_and_save_xmodule(self, location, definition_data=None, metadata=None, system=None,
""" fields={}):
Save the given xmodule (will either create or update based on whether id already exists).
Pulls out the data definition v metadata v children locally but saves it all.
:param xmodule:
"""
# Save any changes to the xmodule to the MongoKeyValueStore
xmodule.save()
self.collection.save({
'_id': namedtuple_to_son(xmodule.location),
'metadata': own_metadata(xmodule),
'definition': {
'data': xmodule.get_explicitly_set_fields_by_scope(Scope.content),
'children': xmodule.children if xmodule.has_children else []
}
})
# recompute (and update) the metadata inheritance tree which is cached
self.refresh_cached_metadata_inheritance_tree(xmodule.location)
self.fire_updated_modulestore_signal(get_course_id_no_run(xmodule.location), xmodule.location)
def create_and_save_xmodule(self, location, definition_data=None, metadata=None, system=None):
""" """
Create the new xmodule and save it. Does not return the new module because if the caller Create the new xmodule and save it. Does not return the new module because if the caller
will insert it as a child, it's inherited metadata will completely change. The difference will insert it as a child, it's inherited metadata will completely change. The difference
between this and just doing create_xmodule and save_xmodule is this ensures static_tabs get between this and just doing create_xmodule and update_item is this ensures static_tabs get
pointed to by the course. pointed to by the course.
:param location: a Location--must have a category :param location: a Location--must have a category
...@@ -711,9 +708,9 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -711,9 +708,9 @@ class MongoModuleStore(ModuleStoreWriteBase):
""" """
# differs from split mongo in that I believe most of this logic should be above the persistence # differs from split mongo in that I believe most of this logic should be above the persistence
# layer but added it here to enable quick conversion. I'll need to reconcile these. # layer but added it here to enable quick conversion. I'll need to reconcile these.
new_object = self.create_xmodule(location, definition_data, metadata, system) new_object = self.create_xmodule(location, definition_data, metadata, system, fields)
location = new_object.location location = new_object.location
self.save_xmodule(new_object) self.update_item(new_object, allow_not_found=True)
# VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so # VS[compat] cdodge: This is a hack because static_tabs also have references from the course module, so
# if we add one then we need to also add it to the policy information (i.e. metadata) # if we add one then we need to also add it to the policy information (i.e. metadata)
...@@ -728,9 +725,9 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -728,9 +725,9 @@ class MongoModuleStore(ModuleStoreWriteBase):
'url_slug': new_object.location.name 'url_slug': new_object.location.name
}) })
course.tabs = existing_tabs course.tabs = existing_tabs
# Save any changes to the course to the MongoKeyValueStore self.update_item(course)
course.save()
self.update_item(course, '**replace_user**') return new_object
def fire_updated_modulestore_signal(self, course_id, location): def fire_updated_modulestore_signal(self, course_id, location):
""" """
...@@ -787,7 +784,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -787,7 +784,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
if result['n'] == 0: if result['n'] == 0:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
def update_item(self, xblock, user, allow_not_found=False): def update_item(self, xblock, user=None, allow_not_found=False):
""" """
Update the persisted version of xblock to reflect its current values. Update the persisted version of xblock to reflect its current values.
...@@ -861,7 +858,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -861,7 +858,7 @@ class MongoModuleStore(ModuleStoreWriteBase):
location = Location.ensure_fully_specified(location) location = Location.ensure_fully_specified(location)
items = self.collection.find({'definition.children': location.url()}, items = self.collection.find({'definition.children': location.url()},
{'_id': True}) {'_id': True})
return [i['_id'] for i in items] return [Location(i['_id']) for i in items]
def get_modulestore_type(self, course_id): def get_modulestore_type(self, course_id):
""" """
......
...@@ -92,7 +92,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -92,7 +92,7 @@ class DraftModuleStore(MongoModuleStore):
except ItemNotFoundError: except ItemNotFoundError:
return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth)) return wrap_draft(super(DraftModuleStore, self).get_instance(course_id, location, depth=depth))
def create_xmodule(self, location, definition_data=None, metadata=None, system=None): def create_xmodule(self, location, definition_data=None, metadata=None, system=None, fields={}):
""" """
Create the new xmodule but don't save it. Returns the new module with a draft locator Create the new xmodule but don't save it. Returns the new module with a draft locator
...@@ -104,22 +104,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -104,22 +104,7 @@ class DraftModuleStore(MongoModuleStore):
draft_loc = as_draft(location) draft_loc = as_draft(location)
if draft_loc.category in DIRECT_ONLY_CATEGORIES: if draft_loc.category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(location) raise InvalidVersionError(location)
return super(DraftModuleStore, self).create_xmodule(draft_loc, definition_data, metadata, system) return super(DraftModuleStore, self).create_xmodule(draft_loc, definition_data, metadata, system, fields)
def save_xmodule(self, xmodule):
"""
Save the given xmodule (will either create or update based on whether id already exists).
Pulls out the data definition v metadata v children locally but saves it all.
:param xmodule:
"""
orig_location = xmodule.location
xmodule.location = as_draft(orig_location)
try:
super(DraftModuleStore, self).save_xmodule(xmodule)
finally:
xmodule.location = orig_location
def get_items(self, location, course_id=None, depth=0, qualifiers=None): def get_items(self, location, course_id=None, depth=0, qualifiers=None):
""" """
...@@ -159,7 +144,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -159,7 +144,7 @@ class DraftModuleStore(MongoModuleStore):
if draft_location.category in DIRECT_ONLY_CATEGORIES: if draft_location.category in DIRECT_ONLY_CATEGORIES:
raise InvalidVersionError(source_location) raise InvalidVersionError(source_location)
if not original: if not original:
raise ItemNotFoundError raise ItemNotFoundError(source_location)
original['_id'] = namedtuple_to_son(draft_location) original['_id'] = namedtuple_to_son(draft_location)
try: try:
self.collection.insert(original) self.collection.insert(original)
...@@ -171,7 +156,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -171,7 +156,7 @@ class DraftModuleStore(MongoModuleStore):
return self._load_items([original])[0] return self._load_items([original])[0]
def update_item(self, xblock, user, allow_not_found=False): def update_item(self, xblock, user=None, allow_not_found=False):
""" """
Save the current values to persisted version of the xblock Save the current values to persisted version of the xblock
...@@ -187,7 +172,7 @@ class DraftModuleStore(MongoModuleStore): ...@@ -187,7 +172,7 @@ class DraftModuleStore(MongoModuleStore):
raise raise
xblock.location = draft_loc xblock.location = draft_loc
super(DraftModuleStore, self).update_item(xblock, user) super(DraftModuleStore, self).update_item(xblock, user, allow_not_found)
# don't allow locations to truly represent themselves as draft outside of this file # don't allow locations to truly represent themselves as draft outside of this file
xblock.location = as_published(xblock.location) xblock.location = as_published(xblock.location)
......
...@@ -51,14 +51,13 @@ import logging ...@@ -51,14 +51,13 @@ import logging
import re import re
from importlib import import_module from importlib import import_module
from path import path from path import path
import collections
import copy import copy
from pytz import UTC from pytz import UTC
from xmodule.errortracker import null_error_tracker from xmodule.errortracker import null_error_tracker
from xmodule.x_module import prefer_xmodules
from xmodule.modulestore.locator import ( from xmodule.modulestore.locator import (
BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree, LocalId, Locator BlockUsageLocator, DefinitionLocator, CourseLocator, VersionTree,
LocalId, Locator
) )
from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError from xmodule.modulestore.exceptions import InsufficientSpecificationError, VersionConflictError, DuplicateItemError
from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE from xmodule.modulestore import inheritance, ModuleStoreWriteBase, Location, SPLIT_MONGO_MODULESTORE_TYPE
...@@ -67,7 +66,6 @@ from ..exceptions import ItemNotFoundError ...@@ -67,7 +66,6 @@ from ..exceptions import ItemNotFoundError
from .definition_lazy_loader import DefinitionLazyLoader from .definition_lazy_loader import DefinitionLazyLoader
from .caching_descriptor_system import CachingDescriptorSystem from .caching_descriptor_system import CachingDescriptorSystem
from xblock.fields import Scope from xblock.fields import Scope
from xblock.runtime import Mixologist
from bson.objectid import ObjectId from bson.objectid import ObjectId
from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection from xmodule.modulestore.split_mongo.mongo_connection import MongoConnection
from xblock.core import XBlock from xblock.core import XBlock
...@@ -132,11 +130,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -132,11 +130,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
self.render_template = render_template self.render_template = render_template
self.i18n_service = i18n_service self.i18n_service = i18n_service
# TODO: Don't have a runtime just to generate the appropriate mixin classes (cpennington)
# This is only used by _partition_fields_by_scope, which is only needed because
# the split mongo store is used for item creation as well as item persistence
self.mixologist = Mixologist(self.xblock_mixins)
def cache_items(self, system, base_block_ids, depth=0, lazy=True): def cache_items(self, system, base_block_ids, depth=0, lazy=True):
''' '''
Handles caching of items once inheritance and any other one time Handles caching of items once inheritance and any other one time
...@@ -281,7 +274,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -281,7 +274,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
} }
return envelope return envelope
def get_courses(self, branch='published', qualifiers=None): def get_courses(self, branch='draft', qualifiers=None):
''' '''
Returns a list of course descriptors matching any given qualifiers. Returns a list of course descriptors matching any given qualifiers.
...@@ -291,7 +284,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -291,7 +284,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
Note, this is to find the current head of the named branch type Note, this is to find the current head of the named branch type
(e.g., 'draft'). To get specific versions via guid use get_course. (e.g., 'draft'). To get specific versions via guid use get_course.
:param branch: the branch for which to return courses. Default value is 'published'. :param branch: the branch for which to return courses. Default value is 'draft'.
:param qualifiers: a optional dict restricting which elements should match :param qualifiers: a optional dict restricting which elements should match
''' '''
if qualifiers is None: if qualifiers is None:
...@@ -563,8 +556,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -563,8 +556,8 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
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.
''' '''
# version_agnostic means we don't care if the head and version don't align, trust the version # course_agnostic means we don't care if the head and version don't align, trust the version
course_struct = self._lookup_course(block_locator.version_agnostic())['structure'] course_struct = self._lookup_course(block_locator.course_agnostic())['structure']
block_id = block_locator.block_id block_id = block_locator.block_id
update_version_field = 'blocks.{}.edit_info.update_version'.format(block_id) update_version_field = 'blocks.{}.edit_info.update_version'.format(block_id)
all_versions_with_block = self.db_connection.find_matching_structures({'original_version': course_struct['original_version'], all_versions_with_block = self.db_connection.find_matching_structures({'original_version': course_struct['original_version'],
...@@ -759,7 +752,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -759,7 +752,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
index_entry = self._get_index_if_valid(course_or_parent_locator, force, continue_version) index_entry = self._get_index_if_valid(course_or_parent_locator, force, continue_version)
structure = self._lookup_course(course_or_parent_locator)['structure'] structure = self._lookup_course(course_or_parent_locator)['structure']
partitioned_fields = self._partition_fields_by_scope(category, fields) partitioned_fields = self.partition_fields_by_scope(category, fields)
new_def_data = partitioned_fields.get(Scope.content, {}) new_def_data = partitioned_fields.get(Scope.content, {})
# persist the definition if persisted != passed # persist the definition if persisted != passed
if (definition_locator is None or isinstance(definition_locator.definition_id, LocalId)): if (definition_locator is None or isinstance(definition_locator.definition_id, LocalId)):
...@@ -822,14 +815,19 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -822,14 +815,19 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if index_entry is not None: if index_entry is not None:
if not continue_version: if not continue_version:
self._update_head(index_entry, course_or_parent_locator.branch, new_id) self._update_head(index_entry, course_or_parent_locator.branch, new_id)
course_parent = course_or_parent_locator.as_course_locator() item_loc = BlockUsageLocator(
package_id=course_or_parent_locator.package_id,
branch=course_or_parent_locator.branch,
block_id=new_block_id,
)
else: else:
course_parent = None item_loc = BlockUsageLocator(
block_id=new_block_id,
version_guid=new_id,
)
# reconstruct the new_item from the cache # reconstruct the new_item from the cache
return self.get_item(BlockUsageLocator(package_id=course_parent, return self.get_item(item_loc)
block_id=new_block_id,
version_guid=new_id))
def create_course( def create_course(
self, org, prettyid, user_id, id_root=None, fields=None, self, org, prettyid, user_id, id_root=None, fields=None,
...@@ -867,7 +865,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -867,7 +865,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
provide any fields overrides, see above). if not provided, will create a mostly empty course provide any fields overrides, see above). if not provided, will create a mostly empty course
structure with just a category course root xblock. structure with just a category course root xblock.
""" """
partitioned_fields = self._partition_fields_by_scope(root_category, fields) partitioned_fields = self.partition_fields_by_scope(root_category, fields)
block_fields = partitioned_fields.setdefault(Scope.settings, {}) block_fields = partitioned_fields.setdefault(Scope.settings, {})
if Scope.children in partitioned_fields: if Scope.children in partitioned_fields:
block_fields.update(partitioned_fields[Scope.children]) block_fields.update(partitioned_fields[Scope.children])
...@@ -1287,7 +1285,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1287,7 +1285,7 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
if index is None: if index is None:
raise ItemNotFoundError(package_id) raise ItemNotFoundError(package_id)
# this is the only real delete in the system. should it do something else? # this is the only real delete in the system. should it do something else?
log.info("deleting course from split-mongo: %s", package_id) log.info(u"deleting course from split-mongo: %s", package_id)
self.db_connection.delete_course_index(index['_id']) self.db_connection.delete_course_index(index['_id'])
def get_errored_courses(self): def get_errored_courses(self):
...@@ -1494,22 +1492,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase): ...@@ -1494,22 +1492,6 @@ class SplitMongoModuleStore(ModuleStoreWriteBase):
index_entry['versions'][branch] = new_id index_entry['versions'][branch] = new_id
self.db_connection.update_course_index(index_entry) self.db_connection.update_course_index(index_entry)
def _partition_fields_by_scope(self, category, fields):
"""
Return dictionary of {scope: {field1: val, ..}..} for the fields of this potential xblock
:param category: the xblock category
:param fields: the dictionary of {fieldname: value}
"""
if fields is None:
return {}
cls = self.mixologist.mix(XBlock.load_class(category, select=self.xblock_select))
result = collections.defaultdict(dict)
for field_name, value in fields.iteritems():
field = getattr(cls, field_name)
result[field.scope][field_name] = value
return result
def _filter_special_fields(self, fields): def _filter_special_fields(self, fields):
""" """
Remove any fields which split or its kvs computes or adds but does not want persisted. Remove any fields which split or its kvs computes or adds but does not want persisted.
......
...@@ -33,7 +33,6 @@ def mixed_store_config(data_dir, mappings): ...@@ -33,7 +33,6 @@ def mixed_store_config(data_dir, mappings):
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': { 'OPTIONS': {
'mappings': mappings, 'mappings': mappings,
'reference_type': 'Location',
'stores': { 'stores': {
'default': mongo_config['default'], 'default': mongo_config['default'],
'xml': xml_config['default'] 'xml': xml_config['default']
...@@ -219,13 +218,21 @@ class ModuleStoreTestCase(TestCase): ...@@ -219,13 +218,21 @@ class ModuleStoreTestCase(TestCase):
# even if we're using a mixed modulestore # even if we're using a mixed modulestore
store = editable_modulestore() store = editable_modulestore()
if hasattr(store, 'collection'): if hasattr(store, 'collection'):
connection = store.collection.database.connection
store.collection.drop() store.collection.drop()
connection.close()
elif hasattr(store, 'close_all_connections'):
store.close_all_connections()
if contentstore().fs_files: if contentstore().fs_files:
db = contentstore().fs_files.database db = contentstore().fs_files.database
db.connection.drop_database(db) db.connection.drop_database(db)
db.connection.close()
location_mapper = loc_mapper() location_mapper = loc_mapper()
if location_mapper.db: if location_mapper.db:
location_mapper.location_map.drop() location_mapper.location_map.drop()
location_mapper.db.connection.close()
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
......
...@@ -2,8 +2,7 @@ from factory import Factory, lazy_attribute_sequence, lazy_attribute ...@@ -2,8 +2,7 @@ from factory import Factory, lazy_attribute_sequence, lazy_attribute
from factory.containers import CyclicDefinitionError from factory.containers import CyclicDefinitionError
from uuid import uuid4 from uuid import uuid4
from xmodule.modulestore import Location from xmodule.modulestore import Location, prefer_xmodules
from xmodule.x_module import prefer_xmodules
from xblock.core import XBlock from xblock.core import XBlock
...@@ -58,7 +57,7 @@ class CourseFactory(XModuleFactory): ...@@ -58,7 +57,7 @@ class CourseFactory(XModuleFactory):
setattr(new_course, k, v) setattr(new_course, k, v)
# Update the data in the mongo datastore # Update the data in the mongo datastore
store.save_xmodule(new_course) store.update_item(new_course)
return new_course return new_course
...@@ -159,7 +158,7 @@ class ItemFactory(XModuleFactory): ...@@ -159,7 +158,7 @@ class ItemFactory(XModuleFactory):
setattr(module, attr, val) setattr(module, attr, val)
module.save() module.save()
store.save_xmodule(module) store.update_item(module)
if 'detached' not in module._class_tags: if 'detached' not in module._class_tags:
parent.children.append(location.url()) parent.children.append(location.url())
......
from xmodule.modulestore.django import modulestore
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
import factory import factory
from factory.helpers import lazy_attribute
# [dhm] I'm not sure why we're using factory_boy if we're not following its pattern. If anyone class SplitFactory(factory.Factory):
# assumes they can call build, it will completely fail, for example. """
# pylint: disable=W0232 Abstracted superclass which defines modulestore so that there's no dependency on django
class PersistentCourseFactory(factory.Factory): if the caller passes modulestore in kwargs
"""
@lazy_attribute
def modulestore(self):
# Delayed import so that we only depend on django if the caller
# hasn't provided their own modulestore
from xmodule.modulestore.django import modulestore
return modulestore('split')
class PersistentCourseFactory(SplitFactory):
""" """
Create a new course (not a new version of a course, but a whole new index entry). Create a new course (not a new version of a course, but a whole new index entry).
...@@ -23,12 +33,15 @@ class PersistentCourseFactory(factory.Factory): ...@@ -23,12 +33,15 @@ class PersistentCourseFactory(factory.Factory):
# pylint: disable=W0613 # pylint: disable=W0613
@classmethod @classmethod
def _create(cls, target_class, org='testX', prettyid='999', user_id='test_user', master_branch='draft', **kwargs): def _create(cls, target_class, org='testX', prettyid='999', user_id='test_user',
master_branch='draft', id_root=None, **kwargs):
modulestore = kwargs.pop('modulestore')
root_block_id = kwargs.pop('root_block_id', 'course')
# Write the data to the mongo datastore # Write the data to the mongo datastore
new_course = modulestore('split').create_course( new_course = modulestore.create_course(
org, prettyid, user_id, fields=kwargs, id_root=prettyid, org, prettyid, user_id, fields=kwargs, id_root=id_root or prettyid,
master_branch=master_branch) master_branch=master_branch, root_block_id=root_block_id)
return new_course return new_course
...@@ -37,7 +50,7 @@ class PersistentCourseFactory(factory.Factory): ...@@ -37,7 +50,7 @@ class PersistentCourseFactory(factory.Factory):
raise NotImplementedError() raise NotImplementedError()
class ItemFactory(factory.Factory): class ItemFactory(SplitFactory):
FACTORY_FOR = XModuleDescriptor FACTORY_FOR = XModuleDescriptor
display_name = factory.LazyAttributeSequence(lambda o, n: "{} {}".format(o.category, n)) display_name = factory.LazyAttributeSequence(lambda o, n: "{} {}".format(o.category, n))
...@@ -45,7 +58,8 @@ class ItemFactory(factory.Factory): ...@@ -45,7 +58,8 @@ class ItemFactory(factory.Factory):
# pylint: disable=W0613 # pylint: disable=W0613
@classmethod @classmethod
def _create(cls, target_class, parent_location, category='chapter', def _create(cls, target_class, parent_location, category='chapter',
user_id='test_user', definition_locator=None, **kwargs): user_id='test_user', block_id=None, definition_locator=None, force=False,
continue_version=False, **kwargs):
""" """
passes *kwargs* as the new item's field values: passes *kwargs* as the new item's field values:
...@@ -55,8 +69,10 @@ class ItemFactory(factory.Factory): ...@@ -55,8 +69,10 @@ class ItemFactory(factory.Factory):
:param definition_locator (optional): the DescriptorLocator for the definition this uses or branches :param definition_locator (optional): the DescriptorLocator for the definition this uses or branches
""" """
return modulestore('split').create_item( modulestore = kwargs.pop('modulestore')
parent_location, category, user_id, definition_locator, fields=kwargs 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
) )
@classmethod @classmethod
......
...@@ -12,11 +12,11 @@ from xmodule.modulestore.loc_mapper_store import LocMapperStore ...@@ -12,11 +12,11 @@ from xmodule.modulestore.loc_mapper_store import LocMapperStore
from mock import Mock from mock import Mock
class TestLocationMapper(unittest.TestCase): class LocMapperSetupSansDjango(unittest.TestCase):
""" """
Test the location to locator mapper Create and destroy a loc mapper for each test
""" """
loc_store = None
def setUp(self): def setUp(self):
modulestore_options = { modulestore_options = {
'host': 'localhost', 'host': 'localhost',
...@@ -27,14 +27,19 @@ class TestLocationMapper(unittest.TestCase): ...@@ -27,14 +27,19 @@ class TestLocationMapper(unittest.TestCase):
cache_standin = TrivialCache() cache_standin = TrivialCache()
self.instrumented_cache = Mock(spec=cache_standin, wraps=cache_standin) self.instrumented_cache = Mock(spec=cache_standin, wraps=cache_standin)
# pylint: disable=W0142 # pylint: disable=W0142
TestLocationMapper.loc_store = LocMapperStore(self.instrumented_cache, **modulestore_options) LocMapperSetupSansDjango.loc_store = LocMapperStore(self.instrumented_cache, **modulestore_options)
def tearDown(self): def tearDown(self):
dbref = TestLocationMapper.loc_store.db dbref = TestLocationMapper.loc_store.db
dbref.drop_collection(TestLocationMapper.loc_store.location_map) dbref.drop_collection(TestLocationMapper.loc_store.location_map)
dbref.connection.close() dbref.connection.close()
TestLocationMapper.loc_store = None self.loc_store = None
class TestLocationMapper(LocMapperSetupSansDjango):
"""
Test the location to locator mapper
"""
def test_create_map(self): def test_create_map(self):
org = 'foo_org' org = 'foo_org'
course = 'bar_course' course = 'bar_course'
...@@ -125,7 +130,7 @@ class TestLocationMapper(unittest.TestCase): ...@@ -125,7 +130,7 @@ class TestLocationMapper(unittest.TestCase):
) )
test_problem_locn = Location('i4x', org, course, 'problem', 'abc123') test_problem_locn = Location('i4x', org, course, 'problem', 'abc123')
# only one course matches # only one course matches
self.translate_n_check(test_problem_locn, old_style_course_id, new_style_package_id, 'problem2', 'published')
# look for w/ only the Location (works b/c there's only one possible course match). Will force # look for w/ only the Location (works b/c there's only one possible course match). Will force
# cache as default translation for this problemid # cache as default translation for this problemid
self.translate_n_check(test_problem_locn, None, new_style_package_id, 'problem2', 'published') self.translate_n_check(test_problem_locn, None, new_style_package_id, 'problem2', 'published')
...@@ -389,7 +394,7 @@ def loc_mapper(): ...@@ -389,7 +394,7 @@ def loc_mapper():
""" """
Mocks the global location mapper. Mocks the global location mapper.
""" """
return TestLocationMapper.loc_store return LocMapperSetupSansDjango.loc_store
def render_to_template_mock(*_args): def render_to_template_mock(*_args):
......
...@@ -200,13 +200,14 @@ class LocatorTest(TestCase): ...@@ -200,13 +200,14 @@ class LocatorTest(TestCase):
expected_id = 'mit.eecs.6002x' expected_id = 'mit.eecs.6002x'
expected_branch = 'published' expected_branch = 'published'
expected_block_ref = 'HW3' expected_block_ref = 'HW3'
testobj = BlockUsageLocator(package_id=testurn) testobj = BlockUsageLocator(url=testurn)
self.check_block_locn_fields(testobj, 'test_block constructor', self.check_block_locn_fields(testobj, 'test_block constructor',
package_id=expected_id, package_id=expected_id,
branch=expected_branch, branch=expected_branch,
block=expected_block_ref) block=expected_block_ref)
self.assertEqual(str(testobj), testurn) self.assertEqual(str(testobj), testurn)
self.assertEqual(testobj.url(), 'edx://' + testurn) self.assertEqual(testobj.url(), 'edx://' + testurn)
testobj = BlockUsageLocator(url=testurn, version_guid=ObjectId())
agnostic = testobj.version_agnostic() agnostic = testobj.version_agnostic()
self.assertIsNone(agnostic.version_guid) self.assertIsNone(agnostic.version_guid)
self.check_block_locn_fields(agnostic, 'test_block constructor', self.check_block_locn_fields(agnostic, 'test_block constructor',
...@@ -225,7 +226,7 @@ class LocatorTest(TestCase): ...@@ -225,7 +226,7 @@ class LocatorTest(TestCase):
block='lab2', block='lab2',
version_guid=ObjectId(test_id_loc) version_guid=ObjectId(test_id_loc)
) )
agnostic = testobj.version_agnostic() agnostic = testobj.course_agnostic()
self.check_block_locn_fields( self.check_block_locn_fields(
agnostic, 'error parsing URL with version and block', agnostic, 'error parsing URL with version and block',
block='lab2', block='lab2',
......
...@@ -56,6 +56,7 @@ class TestMongoModuleStore(object): ...@@ -56,6 +56,7 @@ class TestMongoModuleStore(object):
def teardownClass(cls): def teardownClass(cls):
if cls.connection: if cls.connection:
cls.connection.drop_database(DB) cls.connection.drop_database(DB)
cls.connection.close()
@staticmethod @staticmethod
def initdb(): def initdb():
......
...@@ -163,8 +163,6 @@ class SplitModuleCourseTests(SplitModuleTest): ...@@ -163,8 +163,6 @@ class SplitModuleCourseTests(SplitModuleTest):
"children") "children")
_verify_published_course(modulestore().get_courses(branch='published')) _verify_published_course(modulestore().get_courses(branch='published'))
# default for branch is 'published'.
_verify_published_course(modulestore().get_courses())
def test_search_qualifiers(self): def test_search_qualifiers(self):
# query w/ search criteria # query w/ search criteria
......
...@@ -11,10 +11,10 @@ from mock import Mock, patch ...@@ -11,10 +11,10 @@ from mock import Mock, patch
from django.utils.timezone import UTC from django.utils.timezone import UTC
from xmodule.xml_module import is_pointer_tag from xmodule.xml_module import is_pointer_tag
from xmodule.modulestore import Location from xmodule.modulestore import Location, only_xmodules
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, LocationReader from xmodule.modulestore.xml import ImportSystem, XMLModuleStore, LocationReader
from xmodule.modulestore.inheritance import compute_inherited_metadata from xmodule.modulestore.inheritance import compute_inherited_metadata
from xmodule.x_module import XModuleMixin, only_xmodules from xmodule.x_module import XModuleMixin
from xmodule.fields import Date from xmodule.fields import Date
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
......
...@@ -9,7 +9,7 @@ from factory import Factory, lazy_attribute, post_generation, Sequence ...@@ -9,7 +9,7 @@ from factory import Factory, lazy_attribute, post_generation, Sequence
from lxml import etree from lxml import etree
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import only_xmodules from xmodule.modulestore import only_xmodules
class XmlImportData(object): class XmlImportData(object):
......
...@@ -26,6 +26,7 @@ from xmodule.errortracker import exc_info_to_str ...@@ -26,6 +26,7 @@ from xmodule.errortracker import exc_info_to_str
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError, InvalidLocationError from xmodule.modulestore.exceptions import ItemNotFoundError, InsufficientSpecificationError, InvalidLocationError
from xmodule.modulestore.locator import BlockUsageLocator from xmodule.modulestore.locator import BlockUsageLocator
from xmodule.exceptions import UndefinedContext
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -580,22 +581,6 @@ class ResourceTemplates(object): ...@@ -580,22 +581,6 @@ class ResourceTemplates(object):
return template return template
def prefer_xmodules(identifier, entry_points):
"""Prefer entry_points from the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
if from_xmodule:
return default_select(identifier, from_xmodule)
else:
return default_select(identifier, entry_points)
def only_xmodules(identifier, entry_points):
"""Only use entry_points that are supplied by the xmodule package"""
from_xmodule = [entry_point for entry_point in entry_points if entry_point.dist.key == 'xmodule']
return default_select(identifier, from_xmodule)
@XBlock.needs("i18n") @XBlock.needs("i18n")
class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
""" """
...@@ -811,7 +796,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -811,7 +796,8 @@ class XModuleDescriptor(XModuleMixin, HTMLSnippet, ResourceTemplates, XBlock):
Returns the XModule corresponding to this descriptor. Expects that the system Returns the XModule corresponding to this descriptor. Expects that the system
already supports all of the attributes needed by xmodules already supports all of the attributes needed by xmodules
""" """
assert self.xmodule_runtime is not None if self.xmodule_runtime is None:
raise UndefinedContext()
assert self.xmodule_runtime.error_descriptor_class is not None assert self.xmodule_runtime.error_descriptor_class is not None
if self.xmodule_runtime.xmodule_instance is None: if self.xmodule_runtime.xmodule_instance is None:
try: try:
......
...@@ -31,7 +31,7 @@ class AnonymousIndexPageTest(ModuleStoreTestCase): ...@@ -31,7 +31,7 @@ class AnonymousIndexPageTest(ModuleStoreTestCase):
self.course = CourseFactory.create() self.course = CourseFactory.create()
self.course.days_early_for_beta = 5 self.course.days_early_for_beta = 5
self.course.enrollment_start = datetime.datetime.now(UTC) + datetime.timedelta(days=3) self.course.enrollment_start = datetime.datetime.now(UTC) + datetime.timedelta(days=3)
self.store.save_xmodule(self.course) self.store.update_item(self.course)
@override_settings(FEATURES=FEATURES_WITH_STARTDATE) @override_settings(FEATURES=FEATURES_WITH_STARTDATE)
def test_none_user_index_access_with_startdate_fails(self): def test_none_user_index_access_with_startdate_fails(self):
......
...@@ -45,7 +45,6 @@ MODULESTORE = { ...@@ -45,7 +45,6 @@ MODULESTORE = {
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': { 'OPTIONS': {
'mappings': {}, 'mappings': {},
'reference_type': 'Location',
'stores': { 'stores': {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
......
...@@ -4,7 +4,7 @@ Settings for bok choy tests ...@@ -4,7 +4,7 @@ Settings for bok choy tests
import os import os
from path import path from path import path
from xmodule.x_module import prefer_xmodules from xmodule.modulestore import prefer_xmodules
CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120 CONFIG_ROOT = path(__file__).abspath().dirname() #pylint: disable=E1120
......
...@@ -32,7 +32,6 @@ MODULESTORE = { ...@@ -32,7 +32,6 @@ MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': { 'OPTIONS': {
'reference_type': 'Location',
'mappings': {}, 'mappings': {},
'stores': { 'stores': {
'default': { 'default': {
......
...@@ -34,7 +34,8 @@ from .discussionsettings import * ...@@ -34,7 +34,8 @@ from .discussionsettings import *
from lms.lib.xblock.mixin import LmsBlockMixin from lms.lib.xblock.mixin import LmsBlockMixin
from xmodule.modulestore.inheritance import InheritanceMixin from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.x_module import XModuleMixin, only_xmodules from xmodule.x_module import XModuleMixin
from xmodule.modulestore import only_xmodules
################################### FEATURES ################################### ################################### FEATURES ###################################
# The display name of the platform to be used in templates/emails/etc. # The display name of the platform to be used in templates/emails/etc.
...@@ -440,7 +441,7 @@ XBLOCK_SELECT_FUNCTION = only_xmodules ...@@ -440,7 +441,7 @@ XBLOCK_SELECT_FUNCTION = only_xmodules
# Use the following lines to allow any xblock in the LMS, # Use the following lines to allow any xblock in the LMS,
# either by uncommenting them here, or adding them to your private.py # either by uncommenting them here, or adding them to your private.py
# from xmodule.x_module import prefer_xmodules # from xmodule.modulestore import prefer_xmodules
# XBLOCK_SELECT_FUNCTION = prefer_xmodules # XBLOCK_SELECT_FUNCTION = prefer_xmodules
#################### Python sandbox ############################################ #################### Python sandbox ############################################
......
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