# -*- coding: utf-8 -*- """A modulestore wrapper It will 'unwrap' ccx keys on the way in and re-wrap them on the way out In practical terms this means that when an object is retrieved from modulestore using a CCXLocator or CCXBlockUsageLocator as the key, the equivalent CourseLocator or BlockUsageLocator will actually be used. And all objects returned from the modulestore will have their keys updated to be the CCX version that was passed in. """ from contextlib import contextmanager from functools import partial from ccx_keys.locator import CCXBlockUsageLocator, CCXLocator from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator from xmodule.modulestore import XMODULE_FIELDS_WITH_USAGE_KEYS def strip_ccx(val): """remove any reference to a CCX from the incoming value return a tuple of the stripped value and the id of the ccx """ retval = val ccx_id = None if isinstance(retval, CCXLocator): ccx_id = retval.ccx retval = retval.to_course_locator() elif isinstance(retval, CCXBlockUsageLocator): ccx_id = retval.course_key.ccx retval = retval.to_block_locator() else: for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS: if hasattr(retval, field_name): stripped_field_value, ccx_id = strip_ccx(getattr(retval, field_name)) setattr(retval, field_name, stripped_field_value) return retval, ccx_id def restore_ccx(val, ccx_id): """restore references to a CCX to the incoming value returns the value converted to a CCX-aware state, using the provided ccx_id """ if isinstance(val, CourseLocator): return CCXLocator.from_course_locator(val, ccx_id) elif isinstance(val, BlockUsageLocator): ccx_key = restore_ccx(val.course_key, ccx_id) val = CCXBlockUsageLocator(ccx_key, val.block_type, val.block_id) for field_name in XMODULE_FIELDS_WITH_USAGE_KEYS: if hasattr(val, field_name): setattr(val, field_name, restore_ccx(getattr(val, field_name), ccx_id)) if hasattr(val, 'children'): val.children = restore_ccx_collection(val.children, ccx_id) return val def restore_ccx_collection(field_value, ccx_id=None): """restore references to a CCX to collections of incoming values returns the original collection with all values converted to a ccx-aware state, using the provided ccx_id """ if ccx_id is None: return field_value if isinstance(field_value, list): field_value = [restore_ccx(fv, ccx_id) for fv in field_value] elif isinstance(field_value, dict): for key, val in field_value.iteritems(): field_value[key] = restore_ccx(val, ccx_id) else: field_value = restore_ccx(field_value, ccx_id) return field_value @contextmanager def remove_ccx(to_strip): """A context manager for wrapping modulestore api methods. yields a stripped value and a function suitable for restoring it """ stripped, ccx = strip_ccx(to_strip) yield stripped, partial(restore_ccx_collection, ccx_id=ccx) class CCXModulestoreWrapper(object): """This class wraps a modulestore The purpose is to remove ccx-specific identifiers during lookup and restore it after retrieval so that data can be stored local to a course, but referenced in app context as ccx-specific """ def __init__(self, modulestore): """wrap the provided modulestore""" self.__dict__['_modulestore'] = modulestore def __getattr__(self, name): """look up missing attributes on the wrapped modulestore""" return getattr(self._modulestore, name) def __setattr__(self, name, value): """set attributes only on the wrapped modulestore""" setattr(self._modulestore, name, value) def __delattr__(self, name): """delete attributes only on the wrapped modulestore""" delattr(self._modulestore, name) def _clean_locator_for_mapping(self, locator): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(locator) as (locator, restore): # pylint: disable=protected-access return restore( self._modulestore._clean_locator_for_mapping(locator) ) def _get_modulestore_for_courselike(self, locator=None): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" if locator is not None: locator, _ = strip_ccx(locator) # pylint: disable=protected-access return self._modulestore._get_modulestore_for_courselike(locator) def fill_in_run(self, course_key): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.fill_in_run(course_key)) def has_item(self, usage_key, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" usage_key, _ = strip_ccx(usage_key) return self._modulestore.has_item(usage_key, **kwargs) def get_item(self, usage_key, depth=0, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(usage_key) as (usage_key, restore): return restore( self._modulestore.get_item(usage_key, depth, **kwargs) ) def get_items(self, course_key, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.get_items(course_key, **kwargs)) def get_course(self, course_key, depth=0, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.get_course( course_key, depth=depth, **kwargs )) def has_course(self, course_id, ignore_case=False, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_id) as (course_id, restore): return restore(self._modulestore.has_course( course_id, ignore_case=ignore_case, **kwargs )) def delete_course(self, course_key, user_id): """ See xmodule.modulestore.__init__.ModuleStoreWrite.delete_course """ course_key, _ = strip_ccx(course_key) return self._modulestore.delete_course(course_key, user_id) def get_parent_location(self, location, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(location) as (location, restore): return restore( self._modulestore.get_parent_location(location, **kwargs) ) def get_block_original_usage(self, usage_key): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(usage_key) as (usage_key, restore): orig_key, version = self._modulestore.get_block_original_usage(usage_key) return restore(orig_key), version def get_modulestore_type(self, course_id): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_id) as (course_id, restore): return restore(self._modulestore.get_modulestore_type(course_id)) def get_orphans(self, course_key, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.get_orphans(course_key, **kwargs)) def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(source_course_id) as (source_course_id, _): with remove_ccx(dest_course_id) as (dest_course_id, dest_restore): return dest_restore(self._modulestore.clone_course( source_course_id, dest_course_id, user_id, fields=fields, **kwargs )) def create_item(self, user_id, course_key, block_type, block_id=None, fields=None, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.create_item( user_id, course_key, block_type, block_id=block_id, fields=fields, **kwargs )) def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(parent_usage_key) as (parent_usage_key, restore): return restore(self._modulestore.create_child( user_id, parent_usage_key, block_type, block_id=block_id, fields=fields, **kwargs )) def import_xblock(self, user_id, course_key, block_type, block_id, fields=None, runtime=None, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.import_xblock( user_id, course_key, block_type, block_id, fields=fields, runtime=runtime, **kwargs )) def copy_from_template(self, source_keys, dest_key, user_id, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(dest_key) as (dest_key, restore): return restore(self._modulestore.copy_from_template( source_keys, dest_key, user_id, **kwargs )) def update_item(self, xblock, user_id, allow_not_found=False, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(xblock) as (xblock, restore): return restore(self._modulestore.update_item( xblock, user_id, allow_not_found=allow_not_found, **kwargs )) def delete_item(self, location, user_id, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(location) as (location, restore): return restore( self._modulestore.delete_item(location, user_id, **kwargs) ) def revert_to_published(self, location, user_id): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(location) as (location, restore): return restore( self._modulestore.revert_to_published(location, user_id) ) def create_xblock(self, runtime, course_key, block_type, block_id=None, fields=None, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(course_key) as (course_key, restore): return restore(self._modulestore.create_xblock( runtime, course_key, block_type, block_id=block_id, fields=fields, **kwargs )) def has_published_version(self, xblock): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(xblock) as (xblock, restore): return restore(self._modulestore.has_published_version(xblock)) def publish(self, location, user_id, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(location) as (location, restore): return restore( self._modulestore.publish(location, user_id, **kwargs) ) def unpublish(self, location, user_id, **kwargs): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(location) as (location, restore): return restore( self._modulestore.unpublish(location, user_id, **kwargs) ) def convert_to_draft(self, location, user_id): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(location) as (location, restore): return restore( self._modulestore.convert_to_draft(location, user_id) ) def has_changes(self, xblock): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" with remove_ccx(xblock) as (xblock, restore): return restore(self._modulestore.has_changes(xblock)) def check_supports(self, course_key, method): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" course_key, _ = strip_ccx(course_key) return self._modulestore.check_supports(course_key, method) @contextmanager def branch_setting(self, branch_setting, course_id=None): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" course_id, _ = strip_ccx(course_id) with self._modulestore.branch_setting(branch_setting, course_id): yield @contextmanager def bulk_operations(self, course_id, emit_signals=True, ignore_case=False): """See the docs for xmodule.modulestore.mixed.MixedModuleStore""" course_id, _ = strip_ccx(course_id) with self._modulestore.bulk_operations(course_id, emit_signals=emit_signals, ignore_case=ignore_case): yield