Commit 432249e9 by Don Mitchell

making mixed ms capable of front ending all modulestores for most operations including CRUD

STUD-600
parent a1dc347c
...@@ -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)
......
...@@ -23,7 +23,7 @@ from xblock.exceptions import NoSuchHandlerError ...@@ -23,7 +23,7 @@ from xblock.exceptions import NoSuchHandlerError
from xblock.fields import Scope from xblock.fields import Scope
from xblock.plugin import PluginMissingError from xblock.plugin import PluginMissingError
from xblock.runtime import Mixologist from xblock.runtime import Mixologist
from xmodule.x_module import prefer_xmodules from xmodule.modulestore import prefer_xmodules
from lms.lib.xblock.runtime import unquote_slashes from lms.lib.xblock.runtime import unquote_slashes
...@@ -370,6 +370,6 @@ def component_handler(request, usage_id, handler, suffix=''): ...@@ -370,6 +370,6 @@ 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) modulestore().update_item(descriptor)
return webob_to_django_response(resp) return webob_to_django_response(resp)
...@@ -521,8 +521,8 @@ def orphan_handler(request, tag=None, package_id=None, branch=None, version_guid ...@@ -521,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()
......
...@@ -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 #############################
...@@ -220,7 +221,7 @@ XBLOCK_SELECT_FUNCTION = only_xmodules ...@@ -220,7 +221,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
......
...@@ -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')
...@@ -529,9 +533,75 @@ class ModuleStoreReadBase(ModuleStoreRead): ...@@ -529,9 +533,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}})
......
...@@ -625,7 +625,16 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -625,7 +625,16 @@ 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, location, definition_data=None, metadata=None, runtime=None):
"""
Create a course with the given location. The location category must be 'course'.
"""
if location.category != 'course':
raise ValueError(u"Course roots must be of category 'course': {}".format(unicode(location)))
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 +681,18 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -672,36 +681,18 @@ class MongoModuleStore(ModuleStoreWriteBase):
ScopeIds(None, location.category, location, location), ScopeIds(None, location.category, location, location),
dbmodel, dbmodel,
) )
for key, value in fields.iteritems():
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 +702,9 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -711,9 +702,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 +719,9 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -728,9 +719,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 +778,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -787,7 +778,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 +852,7 @@ class MongoModuleStore(ModuleStoreWriteBase): ...@@ -861,7 +852,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):
""" """
......
...@@ -106,21 +106,6 @@ class DraftModuleStore(MongoModuleStore): ...@@ -106,21 +106,6 @@ class DraftModuleStore(MongoModuleStore):
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)
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):
""" """
Returns a list of XModuleDescriptor instances for the items Returns a list of XModuleDescriptor instances for the items
...@@ -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
...@@ -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,7 @@ def mixed_store_config(data_dir, mappings): ...@@ -33,7 +33,7 @@ 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', 'reference_type': 'xmodule.modulestore.Location',
'stores': { 'stores': {
'default': mongo_config['default'], 'default': mongo_config['default'],
'xml': xml_config['default'] 'xml': xml_config['default']
...@@ -220,12 +220,17 @@ class ModuleStoreTestCase(TestCase): ...@@ -220,12 +220,17 @@ class ModuleStoreTestCase(TestCase):
store = editable_modulestore() store = editable_modulestore()
if hasattr(store, 'collection'): if hasattr(store, 'collection'):
store.collection.drop() store.collection.drop()
store.db.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):
......
...@@ -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():
......
...@@ -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):
......
...@@ -580,22 +580,6 @@ class ResourceTemplates(object): ...@@ -580,22 +580,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):
""" """
......
...@@ -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,7 @@ MODULESTORE = { ...@@ -45,7 +45,7 @@ MODULESTORE = {
'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore', 'ENGINE': 'xmodule.modulestore.mixed.MixedModuleStore',
'OPTIONS': { 'OPTIONS': {
'mappings': {}, 'mappings': {},
'reference_type': 'Location', 'reference_type': 'xmodule.modulestore.Location',
'stores': { 'stores': {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
......
...@@ -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.
......
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