def _load_mixed_class(category): """ Load an XBlock by category name, and apply all defined mixins """ component_class = XBlock.load_class(category, select=settings.XBLOCK_SELECT_FUNCTION) mixologist = Mixologist(settings.XBLOCK_MIXINS) return mixologist.mix(component_class)
def load_mixed_class(category): """ Load an XBlock by category name, and apply all defined mixins """ component_class = XModuleDescriptor.load_class(category) mixologist = Mixologist(settings.XBLOCK_MIXINS) return mixologist.mix(component_class)
def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **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 test_get_uses_class_name_if_block_settings_key_is_not_set(self): """ Test if settings service uses class name if block_settings_key attribute does not exist """ mixologist = Mixologist([]) block = mixologist.mix(_DummyBlock) self.assertEqual(getattr(settings, 'XBLOCK_SETTINGS'), {"_DummyBlock": [1, 2, 3]}) self.assertEqual(self.settings_service.get_settings_bucket(block), [1, 2, 3])
def test_multiply_mixed(self): mixalot = Mixologist([ThirdMixin, FirstMixin]) pre_mixed = mixalot.mix(self.mixologist.mix(FieldTester)) post_mixed = self.mixologist.mix(mixalot.mix(FieldTester)) assert pre_mixed.fields['field'] is FirstMixin.field assert post_mixed.fields['field'] is ThirdMixin.field assert FieldTester is pre_mixed.unmixed_class assert FieldTester is post_mixed.unmixed_class assert len(pre_mixed.__bases__) == 4 # 1 for the original class + 3 mixin classes assert len(post_mixed.__bases__) == 4
def test_multiply_mixed(self): mixalot = Mixologist([ThirdMixin, FirstMixin]) pre_mixed = mixalot.mix(self.mixologist.mix(FieldTester)) post_mixed = self.mixologist.mix(mixalot.mix(FieldTester)) assert_is(pre_mixed.fields['field'], FirstMixin.field) assert_is(post_mixed.fields['field'], ThirdMixin.field) assert_is(FieldTester, pre_mixed.unmixed_class) assert_is(FieldTester, post_mixed.unmixed_class) assert_equals(4, len(pre_mixed.__bases__)) # 1 for the original class + 3 mixin classes assert_equals(4, len(post_mixed.__bases__))
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' 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
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' 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
class TestMixologist(object): """Test that the Mixologist class behaves correctly.""" def setUp(self): self.mixologist = Mixologist([FirstMixin, SecondMixin]) # Test that the classes generated by the mixologist are cached # (and only generated once) def test_only_generate_classes_once(self): assert_is( self.mixologist.mix(FieldTester), self.mixologist.mix(FieldTester), ) assert_is_not( self.mixologist.mix(FieldTester), self.mixologist.mix(TestXBlock), ) # Test that mixins are applied in order def test_mixin_order(self): assert_is(1, self.mixologist.mix(FieldTester).number) assert_is(1, self.mixologist.mix(FieldTester).fields['field'].default) def test_unmixed_class(self): assert_is(FieldTester, self.mixologist.mix(FieldTester).unmixed_class) def test_mixin_fields(self): assert_is(FirstMixin.fields['field'], FirstMixin.field) def test_mixed_fields(self): mixed = self.mixologist.mix(FieldTester) assert_is(mixed.fields['field'], FirstMixin.field) assert_is(mixed.fields['field_a'], FieldTester.field_a)
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' 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 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 org, offering, 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 org, offering, 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 setup_method(self): """ Setup for each test method in this class. """ self.mixologist = Mixologist([FirstMixin, SecondMixin]) # pylint: disable=attribute-defined-outside-init
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' 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 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 org, offering, 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, 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 user_id: ID of the user deleting the item :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 org, offering, 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 create_and_save_xmodule(self, location, user_id, definition_data=None, metadata=None, runtime=None, fields={}): """ Create the new xmodule and save it. :param location: a Location--must have a category :param user_id: ID of the user creating and saving the xmodule :param definition_data: can be empty. The initial definition_data for the kvs :param metadata: can be empty, the initial metadata for the kvs :param runtime: if you already have an xblock from the course, the xblock.runtime value :param fields: a dictionary of field names and values for the new xmodule """ new_object = self.create_xmodule(location, definition_data, metadata, runtime, fields) self.update_item(new_object, user_id, allow_not_found=True) return new_object
def test_get_uses_class_name_if_block_settings_key_is_not_set(self): """ Test if settings service uses class name if block_settings_key attribute does not exist """ mixologist = Mixologist([]) block = mixologist.mix(_DummyBlock) assert settings.XBLOCK_SETTINGS == {'_DummyBlock': [1, 2, 3]} assert self.settings_service.get_settings_bucket(block) == [1, 2, 3]
class TestMixologist(object): """Test that the Mixologist class behaves correctly.""" def setup_method(self): """ Setup for each test method in this class. """ self.mixologist = Mixologist([FirstMixin, SecondMixin]) # pylint: disable=attribute-defined-outside-init # Test that the classes generated by the mixologist are cached # (and only generated once) def test_only_generate_classes_once(self): assert self.mixologist.mix(FieldTester) is self.mixologist.mix(FieldTester) assert not self.mixologist.mix(FieldTester) is self.mixologist.mix(TestXBlock) # Test that mixins are applied in order def test_mixin_order(self): assert 1 is self.mixologist.mix(FieldTester).number assert 1 is self.mixologist.mix(FieldTester).fields['field'].default def test_unmixed_class(self): assert FieldTester is self.mixologist.mix(FieldTester).unmixed_class def test_mixin_fields(self): assert FirstMixin.fields['field'] is FirstMixin.field # pylint: disable=unsubscriptable-object def test_mixed_fields(self): mixed = self.mixologist.mix(FieldTester) assert mixed.fields['field'] is FirstMixin.field assert mixed.fields['field_a'] is FieldTester.field_a def test_duplicate_mixins(self): singly_mixed = self.mixologist.mix(FieldTester) doubly_mixed = self.mixologist.mix(singly_mixed) assert singly_mixed is doubly_mixed assert FieldTester is singly_mixed.unmixed_class def test_multiply_mixed(self): mixalot = Mixologist([ThirdMixin, FirstMixin]) pre_mixed = mixalot.mix(self.mixologist.mix(FieldTester)) post_mixed = self.mixologist.mix(mixalot.mix(FieldTester)) assert pre_mixed.fields['field'] is FirstMixin.field assert post_mixed.fields['field'] is ThirdMixin.field assert FieldTester is pre_mixed.unmixed_class assert FieldTester is post_mixed.unmixed_class assert len(pre_mixed.__bases__) == 4 # 1 for the original class + 3 mixin classes assert len(post_mixed.__bases__) == 4
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **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} """ result = collections.defaultdict(dict) if fields is None: return result cls = self.mixologist.mix(XBlock.load_class(category, select=prefer_xmodules)) for field_name, value in fields.iteritems(): field = getattr(cls, field_name) result[field.scope][field_name] = value return result def create_course(self, org, course, run, user_id, fields=None, runtime=None, **kwargs): """ Creates any necessary other things for the course as a side effect and doesn't return anything useful. The real subclass should call this before it returns the course. """ # clone a default 'about' overview module as well about_location = self.make_course_key(org, course, run).make_usage_key('about', 'overview') about_descriptor = XBlock.load_class('about') overview_template = about_descriptor.get_template('overview.yaml') self.create_item( user_id, about_location.course_key, about_location.block_type, block_id=about_location.block_id, definition_data={'data': overview_template.get('data')}, metadata=overview_template.get('metadata'), runtime=runtime, continue_version=True, ) def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs): """ This base method just copies the assets. The lower level impls must do the actual cloning of content. """ # copy the assets if self.contentstore: self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) return dest_course_id def delete_course(self, course_key, user_id, **kwargs): """ This base method just deletes the assets. The lower level impls must do the actual deleting of content. """ # delete the assets if self.contentstore: self.contentstore.delete_all_course_assets(course_key) super(ModuleStoreWriteBase, self).delete_course(course_key, user_id) def _drop_database(self): """ A destructive operation to drop the underlying database and close all connections. Intended to be used by test code for cleanup. """ if self.contentstore: self.contentstore._drop_database() # pylint: disable=protected-access super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs): """ Creates and saves a new xblock that as a child of the specified block Returns the newly created item. Args: user_id: ID of the user creating and saving the xmodule parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the block that this item should be parented under block_type: The type of block to create block_id: a unique identifier for the new item. If not supplied, a new identifier will be generated fields (dict): A dictionary specifying initial values for some or all fields in the newly created block """ item = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, **kwargs) parent = self.get_item(parent_usage_key) parent.children.append(item.location) self.update_item(parent, user_id) def _find_course_assets(self, course_key): """ Base method to override. """ raise NotImplementedError() def _find_course_asset(self, course_key, filename, get_thumbnail=False): """ Internal; finds or creates course asset info -and- finds existing asset (or thumbnail) metadata. Arguments: course_key (CourseKey): course identifier filename (str): filename of the asset or thumbnail get_thumbnail (bool): True gets thumbnail data, False gets asset data Returns: Asset info for the course, index of asset/thumbnail in list (None if asset/thumbnail does not exist) """ course_assets = self._find_course_assets(course_key) if course_assets is None: return None, None if get_thumbnail: all_assets = course_assets['thumbnails'] else: all_assets = course_assets['assets'] # See if this asset already exists by checking the external_filename. # Studio doesn't currently support using multiple course assets with the same filename. # So use the filename as the unique identifier. for idx, asset in enumerate(all_assets): if asset['filename'] == filename: return course_assets, idx return course_assets, None def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False): """ Base method to over-ride in modulestore. """ raise NotImplementedError() @contract(course_key='CourseKey', asset_metadata='AssetMetadata') def save_asset_metadata(self, course_key, asset_metadata, user_id): """ Saves the asset metadata for a particular course's asset. Arguments: course_key (CourseKey): course identifier asset_metadata (AssetMetadata): data about the course asset data Returns: True if metadata save was successful, else False """ return self._save_asset_info(course_key, asset_metadata, user_id, thumbnail=False) @contract(course_key='CourseKey', asset_thumbnail_metadata='AssetThumbnailMetadata') def save_asset_thumbnail_metadata(self, course_key, asset_thumbnail_metadata, user_id): """ Saves the asset thumbnail metadata for a particular course asset's thumbnail. Arguments: course_key (CourseKey): course identifier asset_thumbnail_metadata (AssetThumbnailMetadata): data about the course asset thumbnail Returns: True if thumbnail metadata save was successful, else False """ return self._save_asset_info(course_key, asset_thumbnail_metadata, user_id, thumbnail=True) @contract(asset_key='AssetKey') def _find_asset_info(self, asset_key, thumbnail=False, **kwargs): """ Find the info for a particular course asset/thumbnail. Arguments: asset_key (AssetKey): key containing original asset filename thumbnail (bool): True if finding thumbnail, False if finding asset metadata Returns: asset/thumbnail metadata (AssetMetadata/AssetThumbnailMetadata) -or- None if not found """ course_assets, asset_idx = self._find_course_asset(asset_key.course_key, asset_key.path, thumbnail) if asset_idx is None: return None if thumbnail: info = 'thumbnails' mdata = AssetThumbnailMetadata(asset_key, asset_key.path, **kwargs) else: info = 'assets' mdata = AssetMetadata(asset_key, asset_key.path, **kwargs) all_assets = course_assets[info] mdata.from_mongo(all_assets[asset_idx]) return mdata @contract(asset_key='AssetKey') def find_asset_metadata(self, asset_key, **kwargs): """ Find the metadata for a particular course asset. Arguments: asset_key (AssetKey): key containing original asset filename Returns: asset metadata (AssetMetadata) -or- None if not found """ return self._find_asset_info(asset_key, thumbnail=False, **kwargs) @contract(asset_key='AssetKey') def find_asset_thumbnail_metadata(self, asset_key, **kwargs): """ Find the metadata for a particular course asset. Arguments: asset_key (AssetKey): key containing original asset filename Returns: asset metadata (AssetMetadata) -or- None if not found """ return self._find_asset_info(asset_key, thumbnail=True, **kwargs) @contract(course_key='CourseKey', start='int | None', maxresults='int | None', sort='list | None', get_thumbnails='bool') def _get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, get_thumbnails=False, **kwargs): """ Returns a list of static asset (or thumbnail) metadata for a course. Args: course_key (CourseKey): course identifier start (int): optional - start at this asset number maxresults (int): optional - return at most this many, -1 means no limit sort (array): optional - None means no sort (sort_by (str), sort_order (str)) sort_by - one of 'uploadDate' or 'displayname' sort_order - one of 'ascending' or 'descending' get_thumbnails (bool): True if getting thumbnail metadata, else getting asset metadata Returns: List of AssetMetadata or AssetThumbnailMetadata objects. """ course_assets = self._find_course_assets(course_key) if course_assets is None: # If no course assets are found, return None instead of empty list # to distinguish zero assets from "not able to retrieve assets". return None if get_thumbnails: all_assets = course_assets.get('thumbnails', []) else: all_assets = course_assets.get('assets', []) # DO_NEXT: Add start/maxresults/sort functionality as part of https://openedx.atlassian.net/browse/PLAT-74 if start and maxresults and sort: pass ret_assets = [] for asset in all_assets: if get_thumbnails: thumb = AssetThumbnailMetadata( course_key.make_asset_key('thumbnail', asset['filename']), internal_name=asset['filename'], **kwargs ) ret_assets.append(thumb) else: asset = AssetMetadata( course_key.make_asset_key('asset', asset['filename']), basename=asset['filename'], edited_on=asset['edit_info']['edited_on'], contenttype=asset['contenttype'], md5=str(asset['md5']), **kwargs ) ret_assets.append(asset) return ret_assets @contract(course_key='CourseKey', start='int | None', maxresults='int | None', sort='list | None') def get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, **kwargs): """ Returns a list of static assets for a course. By default all assets are returned, but start and maxresults can be provided to limit the query. Args: course_key (CourseKey): course identifier start (int): optional - start at this asset number maxresults (int): optional - return at most this many, -1 means no limit sort (array): optional - None means no sort (sort_by (str), sort_order (str)) sort_by - one of 'uploadDate' or 'displayname' sort_order - one of 'ascending' or 'descending' Returns: List of AssetMetadata objects. """ return self._get_all_asset_metadata(course_key, start, maxresults, sort, get_thumbnails=False, **kwargs) @contract(course_key='CourseKey') def get_all_asset_thumbnail_metadata(self, course_key, **kwargs): """ Returns a list of thumbnails for all course assets. Args: course_key (CourseKey): course identifier Returns: List of AssetThumbnailMetadata objects. """ return self._get_all_asset_metadata(course_key, get_thumbnails=True, **kwargs) def set_asset_metadata_attrs(self, asset_key, attrs, user_id): """ Base method to over-ride in modulestore. """ raise NotImplementedError() def _delete_asset_data(self, asset_key, user_id, thumbnail=False): """ Base method to over-ride in modulestore. """ raise NotImplementedError() @contract(asset_key='AssetKey', attr=str) def set_asset_metadata_attr(self, asset_key, attr, value, user_id): """ Add/set the given attr on the asset at the given location. Value can be any type which pymongo accepts. Arguments: asset_key (AssetKey): asset identifier attr (str): which attribute to set value: the value to set it to (any type pymongo accepts such as datetime, number, string) Raises: ItemNotFoundError if no such item exists AttributeError is attr is one of the build in attrs. """ return self.set_asset_metadata_attrs(asset_key, {attr: value}, user_id) @contract(asset_key='AssetKey') def delete_asset_metadata(self, asset_key, user_id): """ Deletes a single asset's metadata. Arguments: asset_key (AssetKey): locator containing original asset filename Returns: Number of asset metadata entries deleted (0 or 1) """ return self._delete_asset_data(asset_key, user_id, thumbnail=False) @contract(asset_key='AssetKey') def delete_asset_thumbnail_metadata(self, asset_key, user_id): """ Deletes a single asset's metadata. Arguments: asset_key (AssetKey): locator containing original asset filename Returns: Number of asset metadata entries deleted (0 or 1) """ return self._delete_asset_data(asset_key, user_id, thumbnail=True) @contract(source_course_key='CourseKey', dest_course_key='CourseKey') def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id): """ Copy all the course assets from source_course_key to dest_course_key. Arguments: source_course_key (CourseKey): identifier of course to copy from dest_course_key (CourseKey): identifier of course to copy to """ pass
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **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 clone_course(self, source_course_id, dest_course_id, user_id): """ This base method just copies the assets. The lower level impls must do the actual cloning of content. """ # copy the assets if self.contentstore: self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) super(ModuleStoreWriteBase, self).clone_course(source_course_id, dest_course_id, user_id) return dest_course_id def delete_course(self, course_key, user_id): """ This base method just deletes the assets. The lower level impls must do the actual deleting of content. """ # delete the assets if self.contentstore: self.contentstore.delete_all_course_assets(course_key) super(ModuleStoreWriteBase, self).delete_course(course_key, user_id) def _drop_database(self): """ A destructive operation to drop the underlying database and close all connections. Intended to be used by test code for cleanup. """ if self.contentstore: self.contentstore._drop_database() # pylint: disable=protected-access super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs): """ Creates and saves a new xblock that as a child of the specified block Returns the newly created item. Args: user_id: ID of the user creating and saving the xmodule parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the block that this item should be parented under block_type: The type of block to create block_id: a unique identifier for the new item. If not supplied, a new identifier will be generated fields (dict): A dictionary specifying initial values for some or all fields in the newly created block """ item = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, **kwargs) parent = self.get_item(parent_usage_key) parent.children.append(item.location) self.update_item(parent, user_id) @contextmanager def bulk_write_operations(self, course_id): """ A context manager for notifying the store of bulk write events. In the case of Mongo, it temporarily disables refreshing the metadata inheritance tree until the bulk operation is completed. """ # TODO # Make this multi-process-safe if future operations need it. # Right now, only Import Course, Clone Course, and Delete Course use this, so # it's ok if the cached metadata in the memcache is invalid when another # request comes in for the same course. try: if hasattr(self, '_begin_bulk_write_operation'): self._begin_bulk_write_operation(course_id) yield finally: # check for the begin method here, # since it's an error if an end method is not defined when a begin method is if hasattr(self, '_begin_bulk_write_operation'): self._end_bulk_write_operation(course_id)
class TestMixologist(object): """Test that the Mixologist class behaves correctly.""" def setUp(self): self.mixologist = Mixologist([FirstMixin, SecondMixin]) # Test that the classes generated by the mixologist are cached # (and only generated once) def test_only_generate_classes_once(self): assert_is( self.mixologist.mix(FieldTester), self.mixologist.mix(FieldTester), ) assert_is_not( self.mixologist.mix(FieldTester), self.mixologist.mix(TestXBlock), ) # Test that mixins are applied in order def test_mixin_order(self): assert_is(1, self.mixologist.mix(FieldTester).number) assert_is(1, self.mixologist.mix(FieldTester).fields['field'].default) def test_unmixed_class(self): assert_is(FieldTester, self.mixologist.mix(FieldTester).unmixed_class) def test_mixin_fields(self): assert_is(FirstMixin.fields['field'], FirstMixin.field) def test_mixed_fields(self): mixed = self.mixologist.mix(FieldTester) assert_is(mixed.fields['field'], FirstMixin.field) assert_is(mixed.fields['field_a'], FieldTester.field_a) def test_duplicate_mixins(self): singly_mixed = self.mixologist.mix(FieldTester) doubly_mixed = self.mixologist.mix(singly_mixed) assert_is(singly_mixed, doubly_mixed) assert_is(FieldTester, singly_mixed.unmixed_class) def test_multiply_mixed(self): mixalot = Mixologist([ThirdMixin, FirstMixin]) pre_mixed = mixalot.mix(self.mixologist.mix(FieldTester)) post_mixed = self.mixologist.mix(mixalot.mix(FieldTester)) assert_is(pre_mixed.fields['field'], FirstMixin.field) assert_is(post_mixed.fields['field'], ThirdMixin.field) assert_is(FieldTester, pre_mixed.unmixed_class) assert_is(FieldTester, post_mixed.unmixed_class) assert_equals(4, len(pre_mixed.__bases__)) # 1 for the original class + 3 mixin classes assert_equals(4, len(post_mixed.__bases__))
class TestMixologist(object): """Test that the Mixologist class behaves correctly.""" def setUp(self): self.mixologist = Mixologist([FirstMixin, SecondMixin]) # Test that the classes generated by the mixologist are cached # (and only generated once) def test_only_generate_classes_once(self): assert_is( self.mixologist.mix(FieldTester), self.mixologist.mix(FieldTester), ) assert_is_not( self.mixologist.mix(FieldTester), self.mixologist.mix(TestXBlock), ) # Test that mixins are applied in order def test_mixin_order(self): assert_is(1, self.mixologist.mix(FieldTester).number) assert_is(1, self.mixologist.mix(FieldTester).fields['field'].default) def test_unmixed_class(self): assert_is(FieldTester, self.mixologist.mix(FieldTester).unmixed_class) def test_mixin_fields(self): assert_is(FirstMixin.fields['field'], FirstMixin.field) def test_mixed_fields(self): mixed = self.mixologist.mix(FieldTester) assert_is(mixed.fields['field'], FirstMixin.field) assert_is(mixed.fields['field_a'], FieldTester.field_a) def test_duplicate_mixins(self): singly_mixed = self.mixologist.mix(FieldTester) doubly_mixed = self.mixologist.mix(singly_mixed) assert_is(singly_mixed, doubly_mixed) assert_is(FieldTester, singly_mixed.unmixed_class) def test_multiply_mixed(self): mixalot = Mixologist([ThirdMixin, FirstMixin]) pre_mixed = mixalot.mix(self.mixologist.mix(FieldTester)) post_mixed = self.mixologist.mix(mixalot.mix(FieldTester)) assert_is(pre_mixed.fields['field'], FirstMixin.field) assert_is(post_mixed.fields['field'], ThirdMixin.field) assert_is(FieldTester, pre_mixed.unmixed_class) assert_is(FieldTester, post_mixed.unmixed_class) assert_equals(4, len( pre_mixed.__bases__)) # 1 for the original class + 3 mixin classes assert_equals(4, len(post_mixed.__bases__))
class TestMixologist: """Test that the Mixologist class behaves correctly.""" def setup_method(self): """ Setup for each test method in this class. """ self.mixologist = Mixologist( ['xblock.test.test_runtime.FirstMixin', SecondMixin]) # pylint: disable=attribute-defined-outside-init # Test that the classes generated by the mixologist are cached # (and only generated once) def test_only_generate_classes_once(self): assert self.mixologist.mix(FieldTester) is self.mixologist.mix( FieldTester) assert not self.mixologist.mix(FieldTester) is self.mixologist.mix( TestXBlock) # Test that mixins are applied in order def test_mixin_order(self): assert 1 is self.mixologist.mix(FieldTester).number assert 1 is self.mixologist.mix(FieldTester).fields['field'].default def test_unmixed_class(self): assert FieldTester is self.mixologist.mix(FieldTester).unmixed_class def test_mixin_fields(self): assert FirstMixin.fields['field'] is FirstMixin.field # pylint: disable=unsubscriptable-object def test_mixed_fields(self): mixed = self.mixologist.mix(FieldTester) assert mixed.fields['field'] is FirstMixin.field assert mixed.fields['field_a'] is FieldTester.field_a def test_duplicate_mixins(self): singly_mixed = self.mixologist.mix(FieldTester) doubly_mixed = self.mixologist.mix(singly_mixed) assert singly_mixed is doubly_mixed assert FieldTester is singly_mixed.unmixed_class def test_multiply_mixed(self): mixalot = Mixologist([ThirdMixin, FirstMixin]) pre_mixed = mixalot.mix(self.mixologist.mix(FieldTester)) post_mixed = self.mixologist.mix(mixalot.mix(FieldTester)) assert pre_mixed.fields['field'] is FirstMixin.field assert post_mixed.fields['field'] is ThirdMixin.field assert FieldTester is pre_mixed.unmixed_class assert FieldTester is post_mixed.unmixed_class assert len(pre_mixed.__bases__ ) == 4 # 1 for the original class + 3 mixin classes assert len(post_mixed.__bases__) == 4
def test_bad_class_name(self): bad_class_name = 'xblock.test.test_runtime.NonExistentMixin' with pytest.raises( ImportError, match="Couldn't import class .*'{}'".format(bad_class_name)): Mixologist([bad_class_name])
def setup_method(self): """ Setup for each test method in this class. """ self.mixologist = Mixologist( ['xblock.test.test_runtime.FirstMixin', SecondMixin]) # pylint: disable=attribute-defined-outside-init
def setUp(self): self.mixologist = Mixologist([FirstMixin, SecondMixin])
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(**kwargs) self.contentstore = contentstore # 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 update_item(self, xblock, user_id, 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 org, course, run, 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, 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 user_id: ID of the user deleting the item :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 org, course, run, 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 create_and_save_xmodule(self, location, user_id, definition_data=None, metadata=None, runtime=None, fields={}): """ Create the new xmodule and save it. :param location: a Location--must have a category :param user_id: ID of the user creating and saving the xmodule :param definition_data: can be empty. The initial definition_data for the kvs :param metadata: can be empty, the initial metadata for the kvs :param runtime: if you already have an xblock from the course, the xblock.runtime value :param fields: a dictionary of field names and values for the new xmodule """ new_object = self.create_xmodule(location, definition_data, metadata, runtime, fields) self.update_item(new_object, user_id, allow_not_found=True) return new_object def clone_course(self, source_course_id, dest_course_id, user_id): """ This base method just copies the assets. The lower level impls must do the actual cloning of content. """ # copy the assets self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) super(ModuleStoreWriteBase, self).clone_course(source_course_id, dest_course_id, user_id) return dest_course_id def delete_course(self, course_key, user_id): """ This base method just deletes the assets. The lower level impls must do the actual deleting of content. """ # delete the assets self.contentstore.delete_all_course_assets(course_key) super(ModuleStoreWriteBase, self).delete_course(course_key, user_id) @contextmanager def bulk_write_operations(self, course_id): """ A context manager for notifying the store of bulk write events. In the case of Mongo, it temporarily disables refreshing the metadata inheritance tree until the bulk operation is completed. """ # TODO # Make this multi-process-safe if future operations need it. # Right now, only Import Course, Clone Course, and Delete Course use this, so # it's ok if the cached metadata in the memcache is invalid when another # request comes in for the same course. try: if hasattr(self, '_begin_bulk_write_operation'): self._begin_bulk_write_operation(course_id) yield finally: # check for the begin method here, # since it's an error if an end method is not defined when a begin method is if hasattr(self, '_begin_bulk_write_operation'): self._end_bulk_write_operation(course_id)
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **kwargs) 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} """ result = collections.defaultdict(dict) if fields is None: return result cls = self.mixologist.mix( XBlock.load_class(category, select=prefer_xmodules)) for field_name, value in fields.iteritems(): field = getattr(cls, field_name) result[field.scope][field_name] = value return result def create_course(self, org, course, run, user_id, fields=None, runtime=None, **kwargs): """ Creates any necessary other things for the course as a side effect and doesn't return anything useful. The real subclass should call this before it returns the course. """ # clone a default 'about' overview module as well about_location = self.make_course_key(org, course, run).make_usage_key( 'about', 'overview') about_descriptor = XBlock.load_class('about') overview_template = about_descriptor.get_template('overview.yaml') self.create_item( user_id, about_location.course_key, about_location.block_type, block_id=about_location.block_id, definition_data={'data': overview_template.get('data')}, metadata=overview_template.get('metadata'), runtime=runtime, continue_version=True, ) def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs): """ This base method just copies the assets. The lower level impls must do the actual cloning of content. """ # copy the assets if self.contentstore: self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) return dest_course_id def delete_course(self, course_key, user_id, **kwargs): """ This base method just deletes the assets. The lower level impls must do the actual deleting of content. """ # delete the assets if self.contentstore: self.contentstore.delete_all_course_assets(course_key) super(ModuleStoreWriteBase, self).delete_course(course_key, user_id) def _drop_database(self): """ A destructive operation to drop the underlying database and close all connections. Intended to be used by test code for cleanup. """ if self.contentstore: self.contentstore._drop_database() # pylint: disable=protected-access super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs): """ Creates and saves a new xblock that as a child of the specified block Returns the newly created item. Args: user_id: ID of the user creating and saving the xmodule parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the block that this item should be parented under block_type: The type of block to create block_id: a unique identifier for the new item. If not supplied, a new identifier will be generated fields (dict): A dictionary specifying initial values for some or all fields in the newly created block """ item = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, **kwargs) parent = self.get_item(parent_usage_key) parent.children.append(item.location) self.update_item(parent, user_id)
def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **kwargs) self.mixologist = Mixologist(self.xblock_mixins)
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **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} """ result = collections.defaultdict(dict) if fields is None: return result cls = self.mixologist.mix( XBlock.load_class(category, select=prefer_xmodules)) for field_name, value in fields.iteritems(): field = getattr(cls, field_name) result[field.scope][field_name] = value return result def create_course(self, org, course, run, user_id, fields=None, runtime=None, **kwargs): """ Creates any necessary other things for the course as a side effect and doesn't return anything useful. The real subclass should call this before it returns the course. """ # clone a default 'about' overview module as well about_location = self.make_course_key(org, course, run).make_usage_key( 'about', 'overview') about_descriptor = XBlock.load_class('about') overview_template = about_descriptor.get_template('overview.yaml') self.create_item( user_id, about_location.course_key, about_location.block_type, block_id=about_location.block_id, definition_data={'data': overview_template.get('data')}, metadata=overview_template.get('metadata'), runtime=runtime, continue_version=True, ) def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs): """ This base method just copies the assets. The lower level impls must do the actual cloning of content. """ # copy the assets if self.contentstore: self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) return dest_course_id def delete_course(self, course_key, user_id, **kwargs): """ This base method just deletes the assets. The lower level impls must do the actual deleting of content. """ # delete the assets if self.contentstore: self.contentstore.delete_all_course_assets(course_key) super(ModuleStoreWriteBase, self).delete_course(course_key, user_id) def _drop_database(self): """ A destructive operation to drop the underlying database and close all connections. Intended to be used by test code for cleanup. """ if self.contentstore: self.contentstore._drop_database() # pylint: disable=protected-access super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs): """ Creates and saves a new xblock that as a child of the specified block Returns the newly created item. Args: user_id: ID of the user creating and saving the xmodule parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the block that this item should be parented under block_type: The type of block to create block_id: a unique identifier for the new item. If not supplied, a new identifier will be generated fields (dict): A dictionary specifying initial values for some or all fields in the newly created block """ item = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, **kwargs) parent = self.get_item(parent_usage_key) parent.children.append(item.location) self.update_item(parent, user_id) @contextmanager def bulk_write_operations(self, course_id): """ A context manager for notifying the store of bulk write events. In the case of Mongo, it temporarily disables refreshing the metadata inheritance tree until the bulk operation is completed. """ # TODO # Make this multi-process-safe if future operations need it. # Right now, only Import Course, Clone Course, and Delete Course use this, so # it's ok if the cached metadata in the memcache is invalid when another # request comes in for the same course. try: if hasattr(self, '_begin_bulk_write_operation'): self._begin_bulk_write_operation(course_id) yield finally: # check for the begin method here, # since it's an error if an end method is not defined when a begin method is if hasattr(self, '_begin_bulk_write_operation'): self._end_bulk_write_operation(course_id)
class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ''' Implement interface functionality that can be shared. ''' def __init__(self, contentstore, **kwargs): super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **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} """ result = collections.defaultdict(dict) if fields is None: return result cls = self.mixologist.mix(XBlock.load_class(category, select=prefer_xmodules)) for field_name, value in fields.iteritems(): field = getattr(cls, field_name) result[field.scope][field_name] = value return result def create_course(self, org, course, run, user_id, fields=None, runtime=None, **kwargs): """ Creates any necessary other things for the course as a side effect and doesn't return anything useful. The real subclass should call this before it returns the course. """ # clone a default 'about' overview module as well about_location = self.make_course_key(org, course, run).make_usage_key('about', 'overview') about_descriptor = XBlock.load_class('about') overview_template = about_descriptor.get_template('overview.yaml') self.create_item( user_id, about_location.course_key, about_location.block_type, block_id=about_location.block_id, definition_data={'data': overview_template.get('data')}, metadata=overview_template.get('metadata'), runtime=runtime, continue_version=True, ) def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs): """ This base method just copies the assets. The lower level impls must do the actual cloning of content. """ # copy the assets if self.contentstore: self.contentstore.copy_all_course_assets(source_course_id, dest_course_id) return dest_course_id def delete_course(self, course_key, user_id, **kwargs): """ This base method just deletes the assets. The lower level impls must do the actual deleting of content. """ # delete the assets if self.contentstore: self.contentstore.delete_all_course_assets(course_key) super(ModuleStoreWriteBase, self).delete_course(course_key, user_id) def _drop_database(self): """ A destructive operation to drop the underlying database and close all connections. Intended to be used by test code for cleanup. """ if self.contentstore: self.contentstore._drop_database() # pylint: disable=protected-access super(ModuleStoreWriteBase, self)._drop_database() # pylint: disable=protected-access def create_child(self, user_id, parent_usage_key, block_type, block_id=None, fields=None, **kwargs): """ Creates and saves a new xblock that as a child of the specified block Returns the newly created item. Args: user_id: ID of the user creating and saving the xmodule parent_usage_key: a :class:`~opaque_key.edx.UsageKey` identifing the block that this item should be parented under block_type: The type of block to create block_id: a unique identifier for the new item. If not supplied, a new identifier will be generated fields (dict): A dictionary specifying initial values for some or all fields in the newly created block """ item = self.create_item(user_id, parent_usage_key.course_key, block_type, block_id=block_id, fields=fields, **kwargs) parent = self.get_item(parent_usage_key) parent.children.append(item.location) self.update_item(parent, user_id)