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}})
......
...@@ -5,15 +5,18 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule ...@@ -5,15 +5,18 @@ In this way, courses can be served up both - say - XMLModuleStore or MongoModule
""" """
from . import ModuleStoreWriteBase import re
from xmodule.modulestore.django import create_modulestore_instance, loc_mapper from importlib import import_module
import logging import logging
from xmodule.modulestore import Location
from xblock.fields import Reference, ReferenceList, String from xblock.fields import Reference, ReferenceList, String
from xmodule.modulestore.locator import CourseLocator, Locator, BlockUsageLocator
from . import ModuleStoreWriteBase
from xmodule.modulestore.django import create_modulestore_instance, loc_mapper
from xmodule.modulestore import Location, SPLIT_MONGO_MODULESTORE_TYPE
from xmodule.modulestore.locator import CourseLocator, BlockUsageLocator
from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError from xmodule.modulestore.exceptions import InsufficientSpecificationError, ItemNotFoundError
from xmodule.modulestore.parsers import ALLOWED_ID_CHARS from xmodule.modulestore.parsers import ALLOWED_ID_CHARS
import re from uuid import uuid4
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -28,7 +31,8 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -28,7 +31,8 @@ class MixedModuleStore(ModuleStoreWriteBase):
Initialize a MixedModuleStore. Here we look into our passed in kwargs which should be a Initialize a MixedModuleStore. Here we look into our passed in kwargs which should be a
collection of other modulestore configuration informations collection of other modulestore configuration informations
:param reference_type: either Location or Locator to indicate what type of reference this app :param reference_type: either a class object such as Locator or Location or the fully
qualified dot-path to that class def to indicate what type of reference the app
uses. uses.
""" """
super(MixedModuleStore, self).__init__(**kwargs) super(MixedModuleStore, self).__init__(**kwargs)
...@@ -39,7 +43,14 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -39,7 +43,14 @@ class MixedModuleStore(ModuleStoreWriteBase):
if reference_type is None: if reference_type is None:
log.warn("reference_type not specified in MixedModuleStore settings. %s", log.warn("reference_type not specified in MixedModuleStore settings. %s",
"Will default temporarily to the to-be-deprecated Location.") "Will default temporarily to the to-be-deprecated Location.")
self.use_locations = (reference_type != 'Locator') self.reference_type = Location
elif isinstance(reference_type, basestring):
module_path, _, class_name = reference_type.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
self.reference_type = class_
else:
self.reference_type = reference_type
if 'default' not in stores: if 'default' not in stores:
raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.') raise Exception('Missing a default modulestore in the MixedModuleStore __init__ method.')
...@@ -58,15 +69,24 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -58,15 +69,24 @@ class MixedModuleStore(ModuleStoreWriteBase):
store['OPTIONS'], store['OPTIONS'],
i18n_service=i18n_service, i18n_service=i18n_service,
) )
# it would be better to have a notion of read-only rather than hardcode
# key name
if is_xml:
self.ensure_loc_maps_exist(key)
def _get_modulestore_for_courseid(self, course_id): def _get_modulestore_for_courseid(self, course_id):
""" """
For a given course_id, look in the mapping table and see if it has been pinned For a given course_id, look in the mapping table and see if it has been pinned
to a particular modulestore to a particular modulestore
""" """
# TODO when this becomes a router capable of handling more than one r/w backend
# we'll need to generalize this to handle mappings from old Locations w/o full
# course_id in much the same way as loc_mapper().translate_location does.
mapping = self.mappings.get(course_id, 'default') mapping = self.mappings.get(course_id, 'default')
return self.modulestores[mapping] return self.modulestores[mapping]
# TODO move the location converters to a helper class which returns a converter object w/ 2
# methods: convert_xblock and convert_reference. Then have mixed get the converter and use it.
def _locator_to_location(self, reference): def _locator_to_location(self, reference):
""" """
Convert the referenced locator to a location casting to and from a string as necessary Convert the referenced locator to a location casting to and from a string as necessary
...@@ -84,14 +104,16 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -84,14 +104,16 @@ class MixedModuleStore(ModuleStoreWriteBase):
stringify = isinstance(reference, basestring) stringify = isinstance(reference, basestring)
if stringify: if stringify:
reference = Location(reference) reference = Location(reference)
locator = loc_mapper().translate_location(course_id, reference, reference.revision == 'draft', True) locator = loc_mapper().translate_location(course_id, reference, reference.revision == None, True)
return unicode(locator) if stringify else locator return unicode(locator) if stringify else locator
def _incoming_reference_adaptor(self, store, course_id, reference): def _incoming_reference_adaptor(self, store, course_id, reference):
""" """
Convert the reference to the type the persistence layer wants Convert the reference to the type the persistence layer wants
""" """
if issubclass(store.reference_type, Location if self.use_locations else Locator): if reference is None:
return None
if issubclass(store.reference_type, self.reference_type):
return reference return reference
if store.reference_type == Location: if store.reference_type == Location:
return self._locator_to_location(reference) return self._locator_to_location(reference)
...@@ -101,7 +123,9 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -101,7 +123,9 @@ class MixedModuleStore(ModuleStoreWriteBase):
""" """
Convert the reference to the type the application wants Convert the reference to the type the application wants
""" """
if issubclass(store.reference_type, Location if self.use_locations else Locator): if reference is None:
return None
if issubclass(store.reference_type, self.reference_type):
return reference return reference
if store.reference_type == Location: if store.reference_type == Location:
return self._location_to_locator(course_id, reference) return self._location_to_locator(course_id, reference)
...@@ -111,6 +135,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -111,6 +135,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
""" """
Change all reference fields in this xblock to the type expected by the receiving layer Change all reference fields in this xblock to the type expected by the receiving layer
""" """
xblock.location = adaptor(store, course_id, xblock.location)
for field in xblock.fields.itervalues(): for field in xblock.fields.itervalues():
if field.is_set_on(xblock): if field.is_set_on(xblock):
if isinstance(field, Reference): if isinstance(field, Reference):
...@@ -136,7 +161,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -136,7 +161,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Change all reference fields in this xblock to the type expected by the persistence layer Change all reference fields in this xblock to the type expected by the persistence layer
""" """
string_converter = self._get_string_converter( string_converter = self._get_string_converter(
course_id, store.reference_type, xblock.location course_id, store.reference_type, xblock.scope_ids.usage_id
) )
return self._xblock_adaptor_iterator( return self._xblock_adaptor_iterator(
self._incoming_reference_adaptor, string_converter, store, course_id, xblock self._incoming_reference_adaptor, string_converter, store, course_id, xblock
...@@ -147,7 +172,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -147,7 +172,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Change all reference fields in this xblock to the type expected by the persistence layer Change all reference fields in this xblock to the type expected by the persistence layer
""" """
string_converter = self._get_string_converter( string_converter = self._get_string_converter(
course_id, xblock.location.__class__, xblock.location course_id, xblock.scope_ids.usage_id.__class__, xblock.scope_ids.usage_id
) )
return self._xblock_adaptor_iterator( return self._xblock_adaptor_iterator(
self._outgoing_reference_adaptor, string_converter, store, course_id, xblock self._outgoing_reference_adaptor, string_converter, store, course_id, xblock
...@@ -160,9 +185,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -160,9 +185,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
Return a closure which finds and replaces all embedded links in a string field Return a closure which finds and replaces all embedded links in a string field
with the correct rewritten link for the target type with the correct rewritten link for the target type
""" """
if self.use_locations and reference_type == Location: if issubclass(self.reference_type, reference_type):
return lambda field, xblock: None
if not self.use_locations and issubclass(reference_type, Locator):
return lambda field, xblock: None return lambda field, xblock: None
if isinstance(from_base_addr, Location): if isinstance(from_base_addr, Location):
def mapper(found_id): def mapper(found_id):
...@@ -189,6 +212,21 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -189,6 +212,21 @@ class MixedModuleStore(ModuleStoreWriteBase):
field.write_to(xblock, value) field.write_to(xblock, value)
return converter return converter
def ensure_loc_maps_exist(self, store_name):
"""
Ensure location maps exist for every course in the modulestore whose
name is the given name (mostly used for 'xml'). It creates maps for any
missing ones.
NOTE: will only work if the given store is Location based. If it's not,
it raises NotImplementedError
"""
store = self.modulestores[store_name]
if store.reference_type != Location:
raise NotImplementedError(u"Cannot create maps from %s", store.reference_type)
for course in store.get_courses():
loc_mapper().translate_location(course.location.course_id, course.location)
def has_item(self, course_id, reference): def has_item(self, course_id, reference):
""" """
Does the course include the xblock who's id is reference? Does the course include the xblock who's id is reference?
...@@ -232,9 +270,8 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -232,9 +270,8 @@ class MixedModuleStore(ModuleStoreWriteBase):
raise Exception("Must pass in a course_id when calling get_items()") raise Exception("Must pass in a course_id when calling get_items()")
store = self._get_modulestore_for_courseid(course_id or getattr(location, 'package_id')) store = self._get_modulestore_for_courseid(course_id or getattr(location, 'package_id'))
# translate won't work w/ missing fields so work around it if not issubclass(self.reference_type, store.reference_type):
if store.reference_type == Location: if store.reference_type == Location:
if not self.use_locations:
if getattr(location, 'block_id', False): if getattr(location, 'block_id', False):
location = self._incoming_reference_adaptor(store, course_id, location) location = self._incoming_reference_adaptor(store, course_id, location)
else: else:
...@@ -245,26 +282,31 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -245,26 +282,31 @@ class MixedModuleStore(ModuleStoreWriteBase):
category=qualifiers.get('category', None), category=qualifiers.get('category', None),
name=None name=None
) )
else: else:
if self.use_locations:
if not isinstance(location, Location): if not isinstance(location, Location):
location = Location(location) location = Location(location)
try: try:
location.ensure_fully_specified() location.ensure_fully_specified()
location = loc_mapper().translate_location( location = loc_mapper().translate_location(
course_id, location, location.revision == 'published', True course_id, location, location.revision == None, True
) )
except InsufficientSpecificationError: except InsufficientSpecificationError:
# construct the Locator by hand # construct the Locator by hand
if location.category is not None and qualifiers.get('category', False): if location.category is not None and qualifiers.get('category', False):
qualifiers['category'] = location.category qualifiers['category'] = location.category
location = loc_mapper().translate_location_to_course_locator( location = loc_mapper().translate_location_to_course_locator(
course_id, location, location.revision == 'published' course_id, location, location.revision == None
) )
xblocks = store.get_items(location, course_id, depth, qualifiers) xblocks = store.get_items(location, course_id, depth, qualifiers)
xblocks = [self._outgoing_xblock_adaptor(store, course_id, xblock) for xblock in xblocks] xblocks = [self._outgoing_xblock_adaptor(store, course_id, xblock) for xblock in xblocks]
return xblocks return xblocks
def _get_course_id_from_course_location(self, course_location):
"""
Get the proper course_id based on the type of course_location
"""
return getattr(course_location, 'course_id', None) or getattr(course_location, 'package_id', None)
def get_courses(self): def get_courses(self):
''' '''
Returns a list containing the top level XModuleDescriptors of the courses Returns a list containing the top level XModuleDescriptors of the courses
...@@ -272,20 +314,31 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -272,20 +314,31 @@ class MixedModuleStore(ModuleStoreWriteBase):
''' '''
courses = [] courses = []
for key in self.modulestores: for key in self.modulestores:
store_courses = self.modulestores[key].get_courses() store = self.modulestores[key]
store_courses = store.get_courses()
# If the store has not been labeled as 'default' then we should # If the store has not been labeled as 'default' then we should
# only surface courses that have a mapping entry, for example the XMLModuleStore will # only surface courses that have a mapping entry, for example the XMLModuleStore will
# slurp up anything that is on disk, however, we don't want to surface those to # slurp up anything that is on disk, however, we don't want to surface those to
# consumers *unless* there is an explicit mapping in the configuration # consumers *unless* there is an explicit mapping in the configuration
# TODO obviously this filtering only applies to filebased stores
if key != 'default': if key != 'default':
for course in store_courses: for course in store_courses:
course_id = self._get_course_id_from_course_location(course.location)
# make sure that the courseId is mapped to the store in question # make sure that the courseId is mapped to the store in question
if key == self.mappings.get(course.location.course_id, 'default'): if key == self.mappings.get(course_id, 'default'):
courses = courses + ([course]) courses.append(
self._outgoing_reference_adaptor(store, course_id, course.location)
)
else: else:
# if we're the 'default' store provider, then we surface all courses hosted in # if we're the 'default' store provider, then we surface all courses hosted in
# that store provider # that store provider
courses = courses + (store_courses) store_courses = [
self._outgoing_reference_adaptor(
store, self._get_course_id_from_course_location(course.location), course.location
)
for course in store_courses
]
courses = courses + store_courses
return courses return courses
...@@ -302,7 +355,7 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -302,7 +355,7 @@ class MixedModuleStore(ModuleStoreWriteBase):
# translate won't work w/ missing fields so work around it # translate won't work w/ missing fields so work around it
if store.reference_type == Location: if store.reference_type == Location:
# takes the course_id: figure out if this is old or new style # takes the course_id: figure out if this is old or new style
if not self.use_locations: if not issubclass(store.reference_type, self.reference_type):
if isinstance(course_id, basestring): if isinstance(course_id, basestring):
course_id = CourseLocator(package_id=course_id, branch='published') course_id = CourseLocator(package_id=course_id, branch='published')
course_location = loc_mapper().translate_locator_to_location(course_id, get_course=True) course_location = loc_mapper().translate_locator_to_location(course_id, get_course=True)
...@@ -333,8 +386,10 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -333,8 +386,10 @@ class MixedModuleStore(ModuleStoreWriteBase):
store = self._get_modulestore_for_courseid(course_id) store = self._get_modulestore_for_courseid(course_id)
decoded_ref = self._incoming_reference_adaptor(store, course_id, location) decoded_ref = self._incoming_reference_adaptor(store, course_id, location)
parents = store.get_parent_locations(decoded_ref, course_id) parents = store.get_parent_locations(decoded_ref, course_id)
return [self._outgoing_reference_adaptor(store, course_id, reference) return [
for reference in parents] self._outgoing_reference_adaptor(store, course_id, reference)
for reference in parents
]
def get_modulestore_type(self, course_id): def get_modulestore_type(self, course_id):
""" """
...@@ -367,16 +422,187 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -367,16 +422,187 @@ class MixedModuleStore(ModuleStoreWriteBase):
errs.update(store.get_errored_courses()) errs.update(store.get_errored_courses())
return errs return errs
def _get_course_id_from_block(self, block, store):
"""
Get the course_id from the block or from asking its store. Expensive.
"""
if block.course_id is not None:
return block.course_id
try:
course = store._get_course_for_item(block.scope_ids.usage_id)
if course:
return course.scope_ids.usage_id.course_id
except: # sorry, that method just raises vanilla Exception
pass
def _infer_course_id_try(self, location):
"""
Create, Update, Delete operations don't require a fully-specified course_id, but
there's no complete & sound general way to compute the course_id except via the
proper modulestore. This method attempts several sound but not complete methods.
:param location: an old style Location
"""
if location.category == 'course': # easiest case
return location.course_id
# try finding in loc_mapper
try:
locator = loc_mapper().translate_location_to_course_locator(None, location)
location = loc_mapper().translate_locator_to_location(locator, get_course=True)
return location.course_id
except ItemNotFoundError:
pass
# expensive query against all location-based modulestores to look for location.
for store in self.modulestores.itervalues():
if isinstance(location, store.reference_type):
try:
block = store.get_item(location)
course_id = self._get_course_id_from_block(block, store)
if course_id is not None:
return course_id
except NotImplementedError:
blocks = store.get_items(location)
if len(blocks) == 1:
block = blocks[0]
if block.course_id is not None:
return block.course_id
except ItemNotFoundError:
pass
# if we get here, it must be in a Locator based store, but we won't be able to find
# it.
return None
def create_course(self, course_location, user_id=None, store_name='default', **kwargs):
"""
Creates and returns the course. It creates a loc map from the course_location to
the new one (if provided as id_root).
NOTE: course_location must be a Location not
a Locator until we no longer need to do loc mapping.
NOTE: unlike the other mixed modulestore methods, this does not adapt its argument
to the persistence store but requires its caller to know what the persistence store
wants for args. It does not translate any references on the way in; so, don't
pass children or other reference fields here.
It does, however, adapt the xblock on the way out to the app's
reference_type
:returns: course xblock
"""
store = self.modulestores[store_name]
if not hasattr(store, 'create_course'):
raise NotImplementedError(u"Cannot create a course on store %s", store_name)
if store.get_modulestore_type(course_location.course_id) == SPLIT_MONGO_MODULESTORE_TYPE:
org = kwargs.pop('org', course_location.org)
pretty_id = kwargs.pop('pretty_id', None)
# TODO rename id_root to package_id for consistency. It's too confusing
id_root = kwargs.pop('id_root', u"{0.org}.{0.course}.{0.name}".format(course_location))
course = store.create_course(
org, pretty_id, user_id, id_root=id_root, master_branch=course_location.revision or 'published',
**kwargs
)
block_map = {course_location.name: {'course': course.location.block_id}}
# NOTE: course.location will be a Locator not == course_location
loc_mapper().create_map_entry(
course_location, course.location.package_id, block_map=block_map
)
else: # assume mongo
course = store.create_course(course_location, **kwargs)
loc_mapper().translate_location(course_location.course_id, course_location)
return self._outgoing_xblock_adaptor(store, course_location.course_id, course)
def create_item(self, course_or_parent_loc, category, user_id=None, **kwargs):
"""
Create and return the item. If parent_loc is a specific location v a course id,
it installs the new item as a child of the parent (if the parent_loc is a specific
xblock reference).
Adds an entry to the loc map using the kwarg location if provided (must be a
Location if provided) or block_id and category if provided.
:param course_or_parent_loc: will be translated appropriately to the course's store.
Can be a course_id (org/course/run), CourseLocator, Location, or BlockUsageLocator.
"""
# find the store for the course
if self.reference_type == Location:
if hasattr(course_or_parent_loc, 'tag'):
course_id = self._infer_course_id_try(course_or_parent_loc)
else:
course_id = course_or_parent_loc
else:
course_id = course_or_parent_loc.package_id
store = self._get_modulestore_for_courseid(course_id)
location = kwargs.pop('location', None)
# invoke its create_item
if store.reference_type == Location:
# convert parent loc if it's legit
block_id = kwargs.pop('block_id', uuid4().hex)
if isinstance(course_or_parent_loc, basestring):
parent_loc = None
if location is None:
locn_dict = Location.parse_course_id(course_id)
locn_dict['category'] = category
locn_dict['name'] = block_id
location = Location(locn_dict)
else:
parent_loc = self._incoming_reference_adaptor(store, course_id, course_or_parent_loc)
# must have a legitimate location, compute if appropriate
if location is None:
location = parent_loc.replace(category=category, name=block_id)
# do the actual creation
xblock = store.create_and_save_xmodule(location, **kwargs)
# add the loc mapping
loc_mapper().translate_location(course_id, location)
# don't forget to attach to parent
if parent_loc is not None and not 'detached' in xblock._class_tags:
parent = store.get_item(parent_loc)
parent.children.append(location.url())
store.update_item(parent)
else:
if isinstance(course_or_parent_loc, basestring): # course_id
old_course_id = course_or_parent_loc
course_or_parent_loc = loc_mapper().translate_location_to_course_locator(
course_or_parent_loc, None
)
elif isinstance(course_or_parent_loc, CourseLocator):
old_course_loc = loc_mapper().translate_locator_to_location(
course_or_parent_loc, get_course=True
)
old_course_id = old_course_loc.course_id
else: # it's a Location
old_course_id = course_id
course_or_parent_loc = self._location_to_locator(course_id, course_or_parent_loc)
fields = kwargs.get('fields', {})
fields.update(kwargs.pop('metadata', {}))
fields.update(kwargs.pop('definition_data', {}))
kwargs['fields'] = fields
xblock = store.create_item(course_or_parent_loc, category, user_id, **kwargs)
if location is None:
locn_dict = Location.parse_course_id(old_course_id)
locn_dict['category'] = category
locn_dict['name'] = xblock.location.block_id
location = Location(locn_dict)
# map location.name to xblock.location.block_id
loc_mapper().translate_location(
old_course_id, location, passed_block_id=xblock.location.block_id
)
return xblock
def update_item(self, xblock, user_id, allow_not_found=False): def update_item(self, xblock, user_id, allow_not_found=False):
""" """
Update the xblock persisted to be the same as the given for all types of fields Update the xblock persisted to be the same as the given for all types of fields
(content, children, and metadata) attribute the change to the given user. (content, children, and metadata) attribute the change to the given user.
""" """
if self.use_locations: if self.reference_type == Location:
raise NotImplementedError course_id = xblock.course_id
if course_id is None:
locator = xblock.location course_id = self._infer_course_id_try(xblock.scope_ids.usage_id)
course_id = locator.package_id if course_id is None:
raise ItemNotFoundError(u"Cannot find modulestore for %s", xblock.scope_ids.usage_id)
else:
locator = xblock.scope_ids.usage_id
course_id = locator.package_id
store = self._get_modulestore_for_courseid(course_id) store = self._get_modulestore_for_courseid(course_id)
# if an xblock, convert its contents to correct addr scheme # if an xblock, convert its contents to correct addr scheme
...@@ -385,13 +611,28 @@ class MixedModuleStore(ModuleStoreWriteBase): ...@@ -385,13 +611,28 @@ class MixedModuleStore(ModuleStoreWriteBase):
return self._outgoing_xblock_adaptor(store, course_id, xblock) return self._outgoing_xblock_adaptor(store, course_id, xblock)
def delete_item(self, location, **kwargs): def delete_item(self, location, user_id=None):
""" """
Delete the given item from persistence. Delete the given item from persistence.
""" """
if self.use_locations: if self.reference_type == Location:
raise NotImplementedError course_id = self._infer_course_id_try(location)
if course_id is None:
raise ItemNotFoundError(u"Cannot find modulestore for %s", location)
else:
course_id = location.package_id
store = self._get_modulestore_for_courseid(course_id)
decoded_ref = self._incoming_reference_adaptor(store, course_id, location)
return store.delete_item(decoded_ref, user_id=user_id)
def close_all_connections(self):
"""
Close all db connections
"""
for mstore in self.modulestores.itervalues():
if hasattr(mstore, 'database'):
mstore.database.connection.close()
elif hasattr(mstore, 'db'):
mstore.db.connection.close()
store = self._get_modulestore_for_courseid(location.package_id)
decoded_ref = self._incoming_reference_adaptor(store, location.package_id, location)
return store.delete_item(decoded_ref, **kwargs)
...@@ -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):
......
# pylint: disable=E0611
from nose.tools import assert_equals, assert_raises, assert_false, \
assert_true, assert_not_equals, assert_in, assert_not_in
# pylint: enable=E0611
import pymongo import pymongo
from uuid import uuid4 from uuid import uuid4
import copy
import ddt
from mock import patch
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from xmodule.modulestore import Location, MONGO_MODULESTORE_TYPE, XML_MODULESTORE_TYPE from xmodule.modulestore import Location, MONGO_MODULESTORE_TYPE, SPLIT_MONGO_MODULESTORE_TYPE, \
XML_MODULESTORE_TYPE
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.xml_importer import import_from_xml
# Mixed modulestore depends on django, so we'll manually configure some django settings from xmodule.modulestore.locator import BlockUsageLocator
# before importing the module from xmodule.modulestore.tests.test_location_mapper import LocMapperSetupSansDjango, loc_mapper
# FIXME remove settings
from django.conf import settings from django.conf import settings
import unittest
import copy
if not settings.configured: if not settings.configured:
settings.configure() settings.configure()
from xmodule.modulestore.mixed import MixedModuleStore from xmodule.modulestore.mixed import MixedModuleStore
@ddt.ddt
HOST = 'localhost' class TestMixedModuleStore(LocMapperSetupSansDjango):
PORT = 27017 """
DB = 'test_mongo_%s' % uuid4().hex[:5] Quasi-superclass which tests Location based apps against both split and mongo dbs (Locator and
COLLECTION = 'modulestore' Location-based dbs)
FS_ROOT = DATA_DIR """
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' HOST = 'localhost'
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': '' PORT = 27017
DB = 'test_mongo_%s' % uuid4().hex[:5]
IMPORT_COURSEID = 'MITx/999/2013_Spring' COLLECTION = 'modulestore'
XML_COURSEID1 = 'edX/toy/2012_Fall' FS_ROOT = DATA_DIR
XML_COURSEID2 = 'edX/simple/2012_Fall' DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
RENDER_TEMPLATE = lambda t_n, d, ctx = None, nsp = 'main': ''
OPTIONS = { REFERENCE_TYPE = 'xmodule.modulestore.Location'
'mappings': {
XML_COURSEID1: 'xml', IMPORT_COURSEID = 'MITx/999/2013_Spring'
XML_COURSEID2: 'xml', XML_COURSEID1 = 'edX/toy/2012_Fall'
IMPORT_COURSEID: 'default' XML_COURSEID2 = 'edX/simple/2012_Fall'
},
'reference_type': 'Location', modulestore_options = {
'stores': { 'default_class': DEFAULT_CLASS,
'xml': { 'fs_root': DATA_DIR,
'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', 'render_template': RENDER_TEMPLATE,
'OPTIONS': { }
'data_dir': DATA_DIR, DOC_STORE_CONFIG = {
'default_class': 'xmodule.hidden_module.HiddenDescriptor', 'host': HOST,
} 'db': DB,
'collection': COLLECTION,
}
OPTIONS = {
'mappings': {
XML_COURSEID1: 'xml',
XML_COURSEID2: 'xml',
IMPORT_COURSEID: 'default'
}, },
'default': { 'reference_type': REFERENCE_TYPE,
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'stores': {
'DOC_STORE_CONFIG': { 'xml': {
'host': HOST, 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore',
'db': DB, 'OPTIONS': {
'collection': COLLECTION, 'data_dir': DATA_DIR,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
}
},
'direct': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
}, },
'OPTIONS': { 'draft': {
'default_class': DEFAULT_CLASS, 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
'fs_root': DATA_DIR, 'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'render_template': RENDER_TEMPLATE, 'OPTIONS': modulestore_options
},
'split': {
'ENGINE': 'xmodule.modulestore.split_mongo.SplitMongoModuleStore',
'DOC_STORE_CONFIG': DOC_STORE_CONFIG,
'OPTIONS': modulestore_options
} }
} }
} }
}
def _compareIgnoreVersion(self, loc1, loc2, msg=None):
"""
AssertEqual replacement for CourseLocator
"""
if not (loc1.package_id == loc2.package_id and loc1.branch == loc2.branch and loc1.block_id == loc2.block_id):
self.fail(self._formatMessage(msg, u"{} != {}".format(unicode(loc1), unicode(loc2))))
class TestMixedModuleStore(object): def setUp(self):
'''Tests!'''
@classmethod
def setupClass(cls):
""" """
Set up the database for testing Set up the database for testing
""" """
cls.connection = pymongo.MongoClient( self.options = getattr(self, 'options', self.OPTIONS)
host=HOST, self.connection = pymongo.MongoClient(
port=PORT, host=self.HOST,
port=self.PORT,
tz_aware=True, tz_aware=True,
) )
cls.connection.drop_database(DB) self.connection.drop_database(self.DB)
cls.fake_location = Location('i4x', 'foo', 'bar', 'vertical', 'baz') self.addCleanup(self.connection.drop_database, self.DB)
import_course_dict = Location.parse_course_id(IMPORT_COURSEID) self.addCleanup(self.connection.close)
cls.import_org = import_course_dict['org']
cls.import_course = import_course_dict['course'] super(TestMixedModuleStore, self).setUp()
cls.import_run = import_course_dict['name']
# NOTE: Creating a single db for all the tests to save time. This patcher = patch('xmodule.modulestore.mixed.loc_mapper', return_value=LocMapperSetupSansDjango.loc_store)
# is ok only as long as none of the tests modify the db. patcher.start()
# If (when!) that changes, need to either reload the db, or load self.addCleanup(patcher.stop)
# once and copy over to a tmp db for each test. self.addTypeEqualityFunc(BlockUsageLocator, '_compareIgnoreVersion')
cls.store = cls.initdb()
def _create_course(self, default, course_location, item_location):
@classmethod
def teardownClass(cls):
""" """
Clear out database after test has completed Create a course w/ one item in the persistence store using the given course & item location.
NOTE: course_location and item_location must be Location regardless of the app reference type in order
to cause the right mapping to be created.
""" """
cls.destroy_db(cls.connection) if default == 'split':
course = self.store.create_course(course_location, store_name=default)
@staticmethod chapter = self.store.create_item(
def initdb(): # don't use course_location as it may not be the repr
""" course.location, item_location.category, location=item_location, block_id=item_location.name
Initialize the database and import one test course into it
"""
# connect to the db
_options = {}
_options.update(OPTIONS)
store = MixedModuleStore(**_options)
import_from_xml(
store._get_modulestore_for_courseid(IMPORT_COURSEID),
DATA_DIR,
['toy'],
target_location_namespace=Location(
'i4x',
TestMixedModuleStore.import_org,
TestMixedModuleStore.import_course,
'course',
TestMixedModuleStore.import_run
) )
) else:
course = self.store.create_course(
return store course_location, store_name=default, metadata={'display_name': course_location.name}
)
chapter = self.store.create_item(course_location, item_location.category, location=item_location)
if self.REFERENCE_TYPE == 'xmodule.modulestore.locator.CourseLocator':
# add_entry is false b/c this is a test that the right thing happened w/o
# wanting any additional side effects
lookup_map = loc_mapper().translate_location(
course_location.course_id, course_location, add_entry_if_missing=False
)
self.assertEqual(lookup_map, course.location)
lookup_map = loc_mapper().translate_location(
course_location.course_id, item_location, add_entry_if_missing=False
)
self.assertEqual(lookup_map, chapter.location)
else:
self.assertEqual(course.location, course_location)
self.assertEqual(chapter.location, item_location)
@staticmethod def initdb(self, default):
def destroy_db(connection):
""" """
Destroy the test db. Initialize the database and create one test course in it
""" """
connection.drop_database(DB) # set the default modulestore
self.options['stores']['default'] = self.options['stores'][default]
def setUp(self): self.store = MixedModuleStore(**self.options)
# make a copy for convenience self.addCleanup(self.store.close_all_connections)
self.connection = TestMixedModuleStore.connection
def generate_location(course_id):
"""
Generate the locations for the given ids
"""
org, course, run = course_id.split('/')
return Location('i4x', org, course, 'course', run)
self.course_locations = {
course_id: generate_location(course_id)
for course_id in [self.IMPORT_COURSEID, self.XML_COURSEID1, self.XML_COURSEID2]
}
self.fake_location = Location('i4x', 'foo', 'bar', 'vertical', 'baz')
self.import_chapter_location = self.course_locations[self.IMPORT_COURSEID].replace(
category='chapter', name='Overview'
)
self.xml_chapter_location = self.course_locations[self.XML_COURSEID1].replace(
category='chapter', name='Overview'
)
# grab old style location b4 possibly converted
import_location = self.course_locations[self.IMPORT_COURSEID]
# get Locators and set up the loc mapper if app is Locator based
if self.REFERENCE_TYPE == 'xmodule.modulestore.locator.CourseLocator':
self.fake_location = loc_mapper().translate_location('foo/bar/2012_Fall', self.fake_location)
self.import_chapter_location = loc_mapper().translate_location(
self.IMPORT_COURSEID, self.import_chapter_location
)
self.xml_chapter_location = loc_mapper().translate_location(
self.XML_COURSEID1, self.xml_chapter_location
)
self.course_locations = {
course_id: loc_mapper().translate_location(course_id, locn)
for course_id, locn in self.course_locations.iteritems()
}
def tearDown(self): self._create_course(default, import_location, self.import_chapter_location)
pass
def test_get_modulestore_type(self): @ddt.data('direct', 'split')
def test_get_modulestore_type(self, default_ms):
""" """
Make sure we get back the store type we expect for given mappings Make sure we get back the store type we expect for given mappings
""" """
assert_equals(self.store.get_modulestore_type(XML_COURSEID1), XML_MODULESTORE_TYPE) self.initdb(default_ms)
assert_equals(self.store.get_modulestore_type(XML_COURSEID2), XML_MODULESTORE_TYPE) self.assertEqual(self.store.get_modulestore_type(self.XML_COURSEID1), XML_MODULESTORE_TYPE)
assert_equals(self.store.get_modulestore_type(IMPORT_COURSEID), MONGO_MODULESTORE_TYPE) self.assertEqual(self.store.get_modulestore_type(self.XML_COURSEID2), XML_MODULESTORE_TYPE)
mongo_ms_type = MONGO_MODULESTORE_TYPE if default_ms == 'direct' else SPLIT_MONGO_MODULESTORE_TYPE
self.assertEqual(self.store.get_modulestore_type(self.IMPORT_COURSEID), mongo_ms_type)
# try an unknown mapping, it should be the 'default' store # try an unknown mapping, it should be the 'default' store
assert_equals(self.store.get_modulestore_type('foo/bar/2012_Fall'), MONGO_MODULESTORE_TYPE) self.assertEqual(self.store.get_modulestore_type('foo/bar/2012_Fall'), mongo_ms_type)
def test_has_item(self): @ddt.data('direct', 'split')
assert_true(self.store.has_item( def test_has_item(self, default_ms):
IMPORT_COURSEID, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run]) self.initdb(default_ms)
)) for course_id, course_locn in self.course_locations.iteritems():
assert_true(self.store.has_item( self.assertTrue(self.store.has_item(course_id, course_locn))
XML_COURSEID1, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall'])
))
# try negative cases # try negative cases
assert_false(self.store.has_item( self.assertFalse(self.store.has_item(self.XML_COURSEID1, self.course_locations[self.IMPORT_COURSEID]))
XML_COURSEID1, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run]) self.assertFalse(self.store.has_item(self.IMPORT_COURSEID, self.course_locations[self.XML_COURSEID1]))
))
assert_false(self.store.has_item(
IMPORT_COURSEID, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall'])
))
def test_get_item(self):
with assert_raises(NotImplementedError):
self.store.get_item(self.fake_location)
def test_get_instance(self): @ddt.data('direct', 'split')
module = self.store.get_instance( def test_get_item(self, default_ms):
IMPORT_COURSEID, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run]) self.initdb(default_ms)
) with self.assertRaises(NotImplementedError):
assert_not_equals(module, None) self.store.get_item(self.fake_location)
module = self.store.get_instance( @ddt.data('direct', 'split')
XML_COURSEID1, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall']) def test_get_instance(self, default_ms):
) self.initdb(default_ms)
assert_not_equals(module, None) for course_id, course_locn in self.course_locations.iteritems():
self.assertIsNotNone(self.store.get_instance(course_id, course_locn))
# try negative cases # try negative cases
with assert_raises(ItemNotFoundError): with self.assertRaises(ItemNotFoundError):
self.store.get_instance( self.store.get_instance(self.XML_COURSEID1, self.course_locations[self.IMPORT_COURSEID])
XML_COURSEID1, Location(['i4x', self.import_org, self.import_course, 'course', self.import_run]) with self.assertRaises(ItemNotFoundError):
) self.store.get_instance(self.IMPORT_COURSEID, self.course_locations[self.XML_COURSEID1])
with assert_raises(ItemNotFoundError): @ddt.data('direct', 'split')
self.store.get_instance( def test_get_items(self, default_ms):
IMPORT_COURSEID, Location(['i4x', 'edX', 'toy', 'course', '2012_Fall']) self.initdb(default_ms)
) for course_id, course_locn in self.course_locations.iteritems():
if hasattr(course_locn, 'as_course_locator'):
def test_get_items(self): locn = course_locn.as_course_locator()
# NOTE: use get_course if you just want the course. get_items only allows wildcarding of category and name else:
modules = self.store.get_items(Location('i4x', None, None, 'course', None), IMPORT_COURSEID) locn = course_locn.replace(org=None, course=None, name=None)
assert_equals(len(modules), 1) # NOTE: use get_course if you just want the course. get_items is expensive
assert_equals(modules[0].location.course, self.import_course) modules = self.store.get_items(locn, course_id, qualifiers={'category': 'course'})
self.assertEqual(len(modules), 1)
modules = self.store.get_items(Location('i4x', None, None, 'course', None), XML_COURSEID1) self.assertEqual(modules[0].location, course_locn)
assert_equals(len(modules), 1)
assert_equals(modules[0].location.course, 'toy') @ddt.data('direct', 'split')
def test_update_item(self, default_ms):
modules = self.store.get_items(Location('i4x', 'edX', 'simple', 'course', None), XML_COURSEID2) """
assert_equals(len(modules), 1) Update should fail for r/o dbs and succeed for r/w ones
assert_equals(modules[0].location.course, 'simple') """
self.initdb(default_ms)
def test_update_item(self): # try a r/o db
# FIXME update if self.REFERENCE_TYPE == 'xmodule.modulestore.locator.CourseLocator':
with assert_raises(NotImplementedError): course_id = self.course_locations[self.XML_COURSEID1]
self.store.update_item(self.fake_location, '**replace_user**') else:
course_id = self.XML_COURSEID1
def test_delete_item(self): course = self.store.get_course(course_id)
with assert_raises(NotImplementedError): # if following raised, then the test is really a noop, change it
self.store.delete_item(self.fake_location) self.assertFalse(course.show_calculator, "Default changed making test meaningless")
course.show_calculator = True
def test_get_courses(self): with self.assertRaises(NotImplementedError):
self.store.update_item(course, None)
# now do it for a r/w db
# get_course api's are inconsistent: one takes Locators the other an old style course id
if hasattr(self.course_locations[self.IMPORT_COURSEID], 'as_course_locator'):
locn = self.course_locations[self.IMPORT_COURSEID]
else:
locn = self.IMPORT_COURSEID
course = self.store.get_course(locn)
# if following raised, then the test is really a noop, change it
self.assertFalse(course.show_calculator, "Default changed making test meaningless")
course.show_calculator = True
self.store.update_item(course, None)
course = self.store.get_course(locn)
self.assertTrue(course.show_calculator)
@ddt.data('direct', 'split')
def test_delete_item(self, default_ms):
"""
Delete should reject on r/o db and work on r/w one
"""
self.initdb(default_ms)
# r/o try deleting the course
with self.assertRaises(NotImplementedError):
self.store.delete_item(self.xml_chapter_location)
self.store.delete_item(self.import_chapter_location, '**replace_user**')
# verify it's gone
with self.assertRaises(ItemNotFoundError):
self.store.get_instance(self.IMPORT_COURSEID, self.import_chapter_location)
@ddt.data('direct', 'split')
def test_get_courses(self, default_ms):
self.initdb(default_ms)
# we should have 3 total courses aggregated # we should have 3 total courses aggregated
courses = self.store.get_courses() courses = self.store.get_courses()
assert_equals(len(courses), 3) self.assertEqual(len(courses), 3)
course_ids = [] course_ids = [course.location for course in courses]
for course in courses: self.assertIn(self.course_locations[self.IMPORT_COURSEID], course_ids)
course_ids.append(course.location.course_id) self.assertIn(self.course_locations[self.XML_COURSEID1], course_ids)
assert_true(IMPORT_COURSEID in course_ids) self.assertIn(self.course_locations[self.XML_COURSEID2], course_ids)
assert_true(XML_COURSEID1 in course_ids)
assert_true(XML_COURSEID2 in course_ids)
def test_xml_get_courses(self): def test_xml_get_courses(self):
""" """
Test that the xml modulestore only loaded the courses from the maps. Test that the xml modulestore only loaded the courses from the maps.
""" """
courses = self.store.modulestores['xml'].get_courses() courses = self.store.modulestores['xml'].get_courses()
assert_equals(len(courses), 2) self.assertEqual(len(courses), 2)
course_ids = [course.location.course_id for course in courses] course_ids = [course.location.course_id for course in courses]
assert_in(XML_COURSEID1, course_ids) self.assertIn(self.XML_COURSEID1, course_ids)
assert_in(XML_COURSEID2, course_ids) self.assertIn(self.XML_COURSEID2, course_ids)
# this course is in the directory from which we loaded courses but not in the map # this course is in the directory from which we loaded courses but not in the map
assert_not_in("edX/toy/TT_2012_Fall", course_ids) self.assertNotIn("edX/toy/TT_2012_Fall", course_ids)
def test_get_course(self): @ddt.data('direct', 'split')
module = self.store.get_course(IMPORT_COURSEID) def test_get_course(self, default_ms):
assert_equals(module.location.course, self.import_course) self.initdb(default_ms)
for course_locn in self.course_locations.itervalues():
module = self.store.get_course(XML_COURSEID1) if hasattr(course_locn, 'as_course_locator'):
assert_equals(module.location.course, 'toy') locn = course_locn.as_course_locator()
else:
module = self.store.get_course(XML_COURSEID2) locn = course_locn.course_id
assert_equals(module.location.course, 'simple') # NOTE: use get_course if you just want the course. get_items is expensive
course = self.store.get_course(locn)
self.assertIsNotNone(course)
self.assertEqual(course.location, course_locn)
# pylint: disable=E1101 # pylint: disable=E1101
def test_get_parent_locations(self): @ddt.data('direct', 'split')
def test_get_parent_locations(self, default_ms):
self.initdb(default_ms)
parents = self.store.get_parent_locations( parents = self.store.get_parent_locations(
Location(['i4x', self.import_org, self.import_course, 'chapter', 'Overview']), self.import_chapter_location,
IMPORT_COURSEID self.IMPORT_COURSEID
) )
assert_equals(len(parents), 1) self.assertEqual(len(parents), 1)
assert_equals(Location(parents[0]).org, self.import_org) self.assertEqual(parents[0], self.course_locations[self.IMPORT_COURSEID])
assert_equals(Location(parents[0]).course, self.import_course)
assert_equals(Location(parents[0]).name, self.import_run)
parents = self.store.get_parent_locations( parents = self.store.get_parent_locations(
Location(['i4x', 'edX', 'toy', 'chapter', 'Overview']), self.xml_chapter_location,
XML_COURSEID1 self.XML_COURSEID1
) )
assert_equals(len(parents), 1) self.assertEqual(len(parents), 1)
assert_equals(Location(parents[0]).org, 'edX') self.assertEqual(parents[0], self.course_locations[self.XML_COURSEID1])
assert_equals(Location(parents[0]).course, 'toy')
assert_equals(Location(parents[0]).name, '2012_Fall')
class TestMixedMSInit(unittest.TestCase): @ddt.ddt
class TestMixedUseLocator(TestMixedModuleStore):
"""
Tests a mixed ms which uses Locators instead of Locations
"""
REFERENCE_TYPE = 'xmodule.modulestore.locator.CourseLocator'
def setUp(self):
self.options = copy.copy(self.OPTIONS)
self.options['reference_type'] = self.REFERENCE_TYPE
super(TestMixedUseLocator, self).setUp()
@ddt.ddt
class TestMixedMSInit(TestMixedModuleStore):
""" """
Test initializing w/o a reference_type Test initializing w/o a reference_type
""" """
REFERENCE_TYPE = None
def setUp(self): def setUp(self):
unittest.TestCase.setUp(self) self.options = copy.copy(self.OPTIONS)
options = copy.copy(OPTIONS) del self.options['reference_type']
del options['reference_type'] super(TestMixedMSInit, self).setUp()
self.connection = pymongo.MongoClient(
host=HOST,
port=PORT,
tz_aware=True,
)
self.store = MixedModuleStore(**options)
def test_use_locations(self): @ddt.data('direct', 'split')
def test_use_locations(self, default_ms):
""" """
Test that use_locations defaulted correctly Test that use_locations defaulted correctly
""" """
self.assertTrue(self.store.use_locations) self.initdb(default_ms)
self.assertEqual(self.store.reference_type, Location)
...@@ -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