Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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)
Beispiel #4
0
    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)
Beispiel #5
0
 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])
Beispiel #6
0
    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
Beispiel #7
0
    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
Beispiel #8
0
    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__))
Beispiel #9
0
    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__))
Beispiel #10
0
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
Beispiel #11
0
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
Beispiel #12
0
    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)
Beispiel #13
0
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)
Beispiel #14
0
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
Beispiel #15
0
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
Beispiel #16
0
 def setup_method(self):
     """
     Setup for each test method in this class.
     """
     self.mixologist = Mixologist([FirstMixin, SecondMixin])  # pylint: disable=attribute-defined-outside-init
Beispiel #17
0
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
Beispiel #18
0
 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]
Beispiel #19
0
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
Beispiel #20
0
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
Beispiel #21
0
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)
Beispiel #22
0
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__))
Beispiel #23
0
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__))
Beispiel #24
0
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
Beispiel #25
0
 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])
Beispiel #26
0
 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])
Beispiel #27
0
 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
Beispiel #28
0
 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)
Beispiel #30
0
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)
Beispiel #31
0
 def __init__(self, contentstore, **kwargs):
     super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore,
                                                **kwargs)
     self.mixologist = Mixologist(self.xblock_mixins)
Beispiel #32
0
 def setUp(self):
     self.mixologist = Mixologist([FirstMixin, SecondMixin])
Beispiel #33
0
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)
Beispiel #34
0
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)
Beispiel #35
0
 def __init__(self, contentstore, **kwargs):
     super(ModuleStoreWriteBase, self).__init__(contentstore=contentstore, **kwargs)
     self.mixologist = Mixologist(self.xblock_mixins)