diff --git a/common/lib/xmodule/xmodule/assetstore/__init__.py b/common/lib/xmodule/xmodule/assetstore/__init__.py index 622b42f264..63b98c7df3 100644 --- a/common/lib/xmodule/xmodule/assetstore/__init__.py +++ b/common/lib/xmodule/xmodule/assetstore/__init__.py @@ -3,11 +3,13 @@ Classes representing asset & asset thumbnail metadata. """ from datetime import datetime +import pytz from contracts import contract, new_contract from opaque_keys.edx.keys import CourseKey, AssetKey new_contract('AssetKey', AssetKey) new_contract('datetime', datetime) +new_contract('basestring', basestring) class IncorrectAssetIdType(Exception): @@ -31,12 +33,13 @@ class AssetMetadata(object): # All AssetMetadata objects should have AssetLocators with this type. ASSET_TYPE = 'asset' - @contract(asset_id='AssetKey', basename='str | unicode | None', internal_name='str | None', locked='bool | None', - contenttype='str | unicode | None', md5='str | None', curr_version='str | None', prev_version='str | None') + @contract(asset_id='AssetKey', basename='basestring | None', internal_name='str | None', locked='bool | None', contenttype='basestring | None', + md5='str | None', curr_version='str | None', prev_version='str | None', edited_by='int | None', edited_on='datetime | None') def __init__(self, asset_id, basename=None, internal_name=None, locked=None, contenttype=None, md5=None, - curr_version=None, prev_version=None): + curr_version=None, prev_version=None, + edited_by=None, edited_on=None, field_decorator=None): """ Construct a AssetMetadata object. @@ -48,10 +51,13 @@ class AssetMetadata(object): contenttype (str): MIME type of the asset. curr_version (str): Current version of the asset. prev_version (str): Previous version of the asset. + edited_by (str): Username of last user to upload this asset. + edited_on (datetime): Datetime of last upload of this asset. + field_decorator (function): used by strip_key to convert OpaqueKeys to the app's understanding """ if asset_id.asset_type != self.ASSET_TYPE: raise IncorrectAssetIdType() - self.asset_id = asset_id + self.asset_id = asset_id if field_decorator is None else field_decorator(asset_id) self.basename = basename # Path w/o filename. self.internal_name = internal_name self.locked = locked @@ -59,8 +65,8 @@ class AssetMetadata(object): self.md5 = md5 self.curr_version = curr_version self.prev_version = prev_version - self.edited_by = None - self.edited_on = None + self.edited_by = edited_by + self.edited_on = edited_on or datetime.now(pytz.utc) def __repr__(self): return """AssetMetadata{!r}""".format(( @@ -131,7 +137,7 @@ class AssetThumbnailMetadata(object): ASSET_TYPE = 'thumbnail' @contract(asset_id='AssetKey', internal_name='str | unicode | None') - def __init__(self, asset_id, internal_name=None): + def __init__(self, asset_id, internal_name=None, field_decorator=None): """ Construct a AssetThumbnailMetadata object. @@ -141,7 +147,7 @@ class AssetThumbnailMetadata(object): """ if asset_id.asset_type != self.ASSET_TYPE: raise IncorrectAssetIdType() - self.asset_id = asset_id + self.asset_id = asset_id if field_decorator is None else field_decorator(asset_id) self.internal_name = internal_name def __repr__(self): diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py index 04684c3bba..4e736ac146 100644 --- a/common/lib/xmodule/xmodule/modulestore/__init__.py +++ b/common/lib/xmodule/xmodule/modulestore/__init__.py @@ -913,7 +913,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): """ raise NotImplementedError() - @contract(course_key='CourseKey', asset_metadata='AssetMetadata', user_id='str | unicode') + @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. @@ -928,7 +928,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): 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): + 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. @@ -939,10 +939,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: True if thumbnail metadata save was successful, else False """ - return self._save_asset_info(course_key, asset_thumbnail_metadata, '', thumbnail=True) + 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): + def _find_asset_info(self, asset_key, thumbnail=False, **kwargs): """ Find the info for a particular course asset/thumbnail. @@ -959,16 +959,16 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): if thumbnail: info = 'thumbnails' - mdata = AssetThumbnailMetadata(asset_key, asset_key.path) + mdata = AssetThumbnailMetadata(asset_key, asset_key.path, **kwargs) else: info = 'assets' - mdata = AssetMetadata(asset_key, asset_key.path) + 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): + def find_asset_metadata(self, asset_key, **kwargs): """ Find the metadata for a particular course asset. @@ -978,10 +978,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: asset metadata (AssetMetadata) -or- None if not found """ - return self._find_asset_info(asset_key, thumbnail=False) + return self._find_asset_info(asset_key, thumbnail=False, **kwargs) @contract(asset_key='AssetKey') - def find_asset_thumbnail_metadata(self, asset_key): + def find_asset_thumbnail_metadata(self, asset_key, **kwargs): """ Find the metadata for a particular course asset. @@ -991,10 +991,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: asset metadata (AssetMetadata) -or- None if not found """ - return self._find_asset_info(asset_key, thumbnail=True) + 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): + 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. @@ -1018,9 +1018,9 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): return None if get_thumbnails: - all_assets = course_assets['thumbnails'] + all_assets = course_assets.get('thumbnails', []) else: - all_assets = course_assets['assets'] + 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: @@ -1029,17 +1029,24 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ret_assets = [] for asset in all_assets: if get_thumbnails: - thumb = AssetThumbnailMetadata(course_key.make_asset_key('thumbnail', asset['filename']), - internal_name=asset['filename']) + thumb = AssetThumbnailMetadata( + course_key.make_asset_key('thumbnail', asset['filename']), + internal_name=asset['filename'], **kwargs + ) ret_assets.append(thumb) else: - one_asset = AssetMetadata(course_key.make_asset_key('asset', asset['filename'])) - one_asset.from_mongo(asset) - ret_assets.append(one_asset) + 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): + 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. @@ -1056,10 +1063,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: List of AssetMetadata objects. """ - return self._get_all_asset_metadata(course_key, start, maxresults, sort, get_thumbnails=False) + 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): + def get_all_asset_thumbnail_metadata(self, course_key, **kwargs): """ Returns a list of thumbnails for all course assets. @@ -1069,7 +1076,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: List of AssetThumbnailMetadata objects. """ - return self._get_all_asset_metadata(course_key, get_thumbnails=True) + return self._get_all_asset_metadata(course_key, get_thumbnails=True, **kwargs) def set_asset_metadata_attrs(self, asset_key, attrs, user_id): """ @@ -1077,7 +1084,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): """ raise NotImplementedError() - def _delete_asset_data(self, asset_key, thumbnail=False): + def _delete_asset_data(self, asset_key, user_id, thumbnail=False): """ Base method to over-ride in modulestore. """ @@ -1100,7 +1107,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): return self.set_asset_metadata_attrs(asset_key, {attr: value}, user_id) @contract(asset_key='AssetKey') - def delete_asset_metadata(self, asset_key): + def delete_asset_metadata(self, asset_key, user_id): """ Deletes a single asset's metadata. @@ -1110,10 +1117,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: Number of asset metadata entries deleted (0 or 1) """ - return self._delete_asset_data(asset_key, thumbnail=False) + return self._delete_asset_data(asset_key, user_id, thumbnail=False) @contract(asset_key='AssetKey') - def delete_asset_thumbnail_metadata(self, asset_key): + def delete_asset_thumbnail_metadata(self, asset_key, user_id): """ Deletes a single asset's metadata. @@ -1123,10 +1130,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): Returns: Number of asset metadata entries deleted (0 or 1) """ - return self._delete_asset_data(asset_key, thumbnail=True) + 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): + 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. diff --git a/common/lib/xmodule/xmodule/modulestore/mixed.py b/common/lib/xmodule/xmodule/modulestore/mixed.py index 5ab6a5c69b..419d7a4dfd 100644 --- a/common/lib/xmodule/xmodule/modulestore/mixed.py +++ b/common/lib/xmodule/xmodule/modulestore/mixed.py @@ -315,24 +315,6 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): store = self._get_modulestore_for_courseid(course_key) return store.delete_course(course_key, user_id) - def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False): - """ - Base method to over-ride in modulestore. - """ - raise NotImplementedError() - - def _delete_asset_data(self, asset_key, thumbnail=False): - """ - Base method to over-ride in modulestore. - """ - raise NotImplementedError() - - def _find_course_assets(self, course_key): - """ - Base method to override. - """ - raise NotImplementedError() - @contract(course_key='CourseKey', asset_metadata='AssetMetadata') def save_asset_metadata(self, course_key, asset_metadata, user_id): """ @@ -341,30 +323,25 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): Args: course_key (CourseKey): course identifier asset_metadata (AssetMetadata): data about the course asset data - - Returns: - bool: True if metadata save was successful, else False """ store = self._get_modulestore_for_courseid(course_key) return store.save_asset_metadata(course_key, asset_metadata, user_id) @contract(course_key='CourseKey', asset_thumbnail_metadata='AssetThumbnailMetadata') - def save_asset_thumbnail_metadata(self, course_key, asset_thumbnail_metadata): + 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 """ store = self._get_modulestore_for_courseid(course_key) - return store.save_asset_metadata(course_key, asset_thumbnail_metadata) + return store.save_asset_thumbnail_metadata(course_key, asset_thumbnail_metadata, user_id) + @strip_key @contract(asset_key='AssetKey') - def find_asset_metadata(self, asset_key): + def find_asset_metadata(self, asset_key, **kwargs): """ Find the metadata for a particular course asset. @@ -375,10 +352,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): asset metadata (AssetMetadata) -or- None if not found """ store = self._get_modulestore_for_courseid(asset_key.course_key) - return store.find_asset_metadata(asset_key) + return store.find_asset_metadata(asset_key, **kwargs) + @strip_key @contract(asset_key='AssetKey') - def find_asset_thumbnail_metadata(self, asset_key): + def find_asset_thumbnail_metadata(self, asset_key, **kwargs): """ Find the metadata for a particular course asset. @@ -389,10 +367,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): asset metadata (AssetMetadata) -or- None if not found """ store = self._get_modulestore_for_courseid(asset_key.course_key) - return store.find_asset_thumbnail_metadata(asset_key) + return store.find_asset_thumbnail_metadata(asset_key, **kwargs) + @strip_key @contract(course_key='CourseKey', start=int, maxresults=int, sort='list | None') - def get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=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. @@ -415,10 +394,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): md5: An md5 hash of the asset content """ store = self._get_modulestore_for_courseid(course_key) - return store.get_all_asset_metadata(course_key, start, maxresults, sort) + return store.get_all_asset_metadata(course_key, start, maxresults, sort, **kwargs) + @strip_key @contract(course_key='CourseKey') - def get_all_asset_thumbnail_metadata(self, course_key): + def get_all_asset_thumbnail_metadata(self, course_key, **kwargs): """ Returns a list of thumbnails for all course assets. @@ -429,10 +409,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): List of AssetThumbnailMetadata objects. """ store = self._get_modulestore_for_courseid(course_key) - return store.get_all_asset_thumbnail_metadata(course_key) + return store.get_all_asset_thumbnail_metadata(course_key, **kwargs) @contract(asset_key='AssetKey') - def delete_asset_metadata(self, asset_key): + def delete_asset_metadata(self, asset_key, user_id): """ Deletes a single asset's metadata. @@ -443,10 +423,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): Number of asset metadata entries deleted (0 or 1) """ store = self._get_modulestore_for_courseid(asset_key.course_key) - return store.delete_asset_metadata(asset_key) + return store.delete_asset_metadata(asset_key, user_id) @contract(asset_key='AssetKey') - def delete_asset_thumbnail_metadata(self, asset_key): + def delete_asset_thumbnail_metadata(self, asset_key, user_id): """ Deletes a single asset's metadata. @@ -457,10 +437,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): Number of asset metadata entries deleted (0 or 1) """ store = self._get_modulestore_for_courseid(asset_key.course_key) - return store.delete_asset_metadata(asset_key) + return store.delete_asset_thumbnail_metadata(asset_key, user_id) @contract(course_key='CourseKey') - def delete_all_asset_metadata(self, course_key): + def delete_all_asset_metadata(self, course_key, user_id): """ Delete all of the assets which use this course_key as an identifier. @@ -468,10 +448,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): course_key (CourseKey): course_identifier """ store = self._get_modulestore_for_courseid(course_key) - return store.delete_all_asset_metadata(course_key) + return store.delete_all_asset_metadata(course_key, user_id) @contract(source_course_key='CourseKey', dest_course_key='CourseKey') - def copy_all_asset_metadata(self, source_course_key, dest_course_key): + 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. @@ -483,7 +463,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): # Check the modulestores of both the source and dest course_keys. If in different modulestores, # export all asset data from one modulestore and import it into the dest one. store = self._get_modulestore_for_courseid(source_course_key) - return store.copy_all_asset_metadata(source_course_key, dest_course_key) + return store.copy_all_asset_metadata(source_course_key, dest_course_key, user_id) @contract(asset_key='AssetKey', attr=str) def set_asset_metadata_attr(self, asset_key, attr, value, user_id): @@ -500,7 +480,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): AttributeError is attr is one of the build in attrs. """ store = self._get_modulestore_for_courseid(asset_key.course_key) - return store.set_asset_metadata_attrs(asset_key, attr, value, user_id) + return store.set_asset_metadata_attrs(asset_key, {attr: value}, user_id) @contract(asset_key='AssetKey', attr_dict=dict) def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id): diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/base.py b/common/lib/xmodule/xmodule/modulestore/mongo/base.py index 4fcb3f0497..ddffb0b70f 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo/base.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo/base.py @@ -1474,7 +1474,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo return course_assets - @contract(course_key='CourseKey', asset_metadata='AssetMetadata | AssetThumbnailMetadata', user_id='str | unicode') + @contract(course_key='CourseKey', asset_metadata='AssetMetadata | AssetThumbnailMetadata') def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False): """ Saves the info for a particular course's asset/thumbnail. @@ -1537,7 +1537,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo md = AssetMetadata(asset_key, asset_key.path) md.from_mongo(all_assets[asset_idx]) md.update(attr_dict) - md.update({'edited_by': user_id, 'edited_on': datetime.now(UTC)}) # Generate a Mongo doc from the metadata and update the course asset info. all_assets[asset_idx] = md.to_mongo() @@ -1545,7 +1544,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo self.asset_collection.update({'_id': course_assets['_id']}, {"$set": {'assets': all_assets}}) @contract(asset_key='AssetKey') - def _delete_asset_data(self, asset_key, thumbnail=False): + def _delete_asset_data(self, asset_key, user_id, thumbnail=False): """ Internal; deletes a single asset's metadata -or- thumbnail. @@ -1572,8 +1571,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_asset_info}}) return 1 + # pylint: disable=unused-argument @contract(course_key='CourseKey') - def delete_all_asset_metadata(self, course_key): + def delete_all_asset_metadata(self, course_key, user_id): """ Delete all of the assets which use this course_key as an identifier. diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 507b2a0ed2..f8888b960c 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -80,6 +80,7 @@ from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope from xmodule.error_module import ErrorDescriptor from collections import defaultdict from types import NoneType +from xmodule.assetstore import AssetMetadata log = logging.getLogger(__name__) @@ -1174,7 +1175,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): Find the version_history_depth next versions of this definition. Return as a VersionTree ''' # TODO implement - raise NotImplementedError() + pass def create_definition_from_data(self, course_key, new_def_data, category, user_id): """ @@ -2120,6 +2121,180 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): """ return ModuleStoreEnum.Type.split + def _find_course_assets(self, course_key): + """ + Split specific lookup + """ + return self._lookup_course(course_key).structure + + def _find_course_asset(self, course_key, filename, get_thumbnail=False): + structure = self._lookup_course(course_key).structure + return structure, self._lookup_course_asset(structure, filename, get_thumbnail) + + def _lookup_course_asset(self, structure, filename, get_thumbnail=False): + """ + Find the course asset in the structure or return None if it does not exist + """ + # 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. + accessor = 'thumbnails' if get_thumbnail else 'assets' + for idx, asset in enumerate(structure.get(accessor, [])): + if asset['filename'] == filename: + return idx + return None + + def _update_course_assets(self, user_id, asset_key, update_function, get_thumbnail=False): + """ + A wrapper for functions wanting to manipulate assets. Gets and versions the structure, + passes the mutable array for either 'assets' or 'thumbnails' as well as the idx to the function for it to + update, then persists the changed data back into the course. + + The update function can raise an exception if it doesn't want to actually do the commit. The + surrounding method probably should catch that exception. + """ + with self.bulk_operations(asset_key.course_key): + original_structure = self._lookup_course(asset_key.course_key).structure + index_entry = self._get_index_if_valid(asset_key.course_key) + new_structure = self.version_structure(asset_key.course_key, original_structure, user_id) + + accessor = 'thumbnails' if get_thumbnail else 'assets' + asset_idx = self._lookup_course_asset(new_structure, asset_key.path, get_thumbnail) + + new_structure[accessor] = update_function(new_structure.get(accessor, []), asset_idx) + + # update index if appropriate and structures + self.update_structure(asset_key.course_key, new_structure) + + if index_entry is not None: + # update the index entry if appropriate + self._update_head(asset_key.course_key, index_entry, asset_key.branch, new_structure['_id']) + + def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False): + """ + The guts of saving a new or updated asset + """ + metadata_to_insert = asset_metadata.to_mongo() + + def _internal_method(all_assets, asset_idx): + """ + Either replace the existing entry or add a new one + """ + if asset_idx is None: + all_assets.append(metadata_to_insert) + else: + all_assets[asset_idx] = metadata_to_insert + return all_assets + + return self._update_course_assets(user_id, asset_metadata.asset_id, _internal_method, thumbnail) + + @contract(asset_key='AssetKey', attr_dict=dict) + def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id): + """ + Add/set the given dict of attrs on the asset at the given location. Value can be any type which pymongo accepts. + + Arguments: + asset_key (AssetKey): asset identifier + attr_dict (dict): attribute: value pairs to set + + Raises: + ItemNotFoundError if no such item exists + AttributeError is attr is one of the build in attrs. + """ + def _internal_method(all_assets, asset_idx): + """ + Update the found item + """ + if asset_idx is None: + raise ItemNotFoundError(asset_key) + + # Form an AssetMetadata. + mdata = AssetMetadata(asset_key, asset_key.path) + mdata.from_mongo(all_assets[asset_idx]) + mdata.update(attr_dict) + + # Generate a Mongo doc from the metadata and update the course asset info. + all_assets[asset_idx] = mdata.to_mongo() + return all_assets + + self._update_course_assets(user_id, asset_key, _internal_method, False) + + @contract(asset_key='AssetKey') + def _delete_asset_data(self, asset_key, user_id, thumbnail=False): + """ + Internal; deletes a single asset's metadata -or- thumbnail. + + Arguments: + asset_key (AssetKey): key containing original asset/thumbnail filename + thumbnail: True if thumbnail deletion, False if asset metadata deletion + + Returns: + Number of asset metadata/thumbnail entries deleted (0 or 1) + """ + def _internal_method(all_asset_info, asset_idx): + """ + Remove the item if it was found + """ + if asset_idx is None: + raise ItemNotFoundError(asset_key) + + all_asset_info.pop(asset_idx) + return all_asset_info + + try: + self._update_course_assets(user_id, asset_key, _internal_method, thumbnail) + return 1 + except ItemNotFoundError: + return 0 + + @contract(course_key='CourseKey') + def delete_all_asset_metadata(self, course_key, user_id): + """ + Delete all of the assets which use this course_key as an identifier. + + Arguments: + course_key (CourseKey): course_identifier + """ + with self.bulk_operations(course_key): + original_structure = self._lookup_course(course_key).structure + index_entry = self._get_index_if_valid(course_key) + new_structure = self.version_structure(course_key, original_structure, user_id) + + new_structure['assets'] = [] + new_structure['thumbnails'] = [] + + # update index if appropriate and structures + self.update_structure(course_key, new_structure) + + if index_entry is not None: + # update the index entry if appropriate + self._update_head(course_key, index_entry, course_key.branch, new_structure['_id']) + + @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 + """ + source_structure = self._lookup_course(source_course_key).structure + with self.bulk_operations(dest_course_key): + original_structure = self._lookup_course(dest_course_key).structure + index_entry = self._get_index_if_valid(dest_course_key) + new_structure = self.version_structure(dest_course_key, original_structure, user_id) + + new_structure['assets'] = source_structure.get('assets', []) + new_structure['thumbnails'] = source_structure.get('thumbnails', []) + + # update index if appropriate and structures + self.update_structure(dest_course_key, new_structure) + + if index_entry is not None: + # update the index entry if appropriate + self._update_head(dest_course_key, index_entry, dest_course_key.branch, new_structure['_id']) + def internal_clean_children(self, course_locator): """ Only intended for rather low level methods to use. Goes through the children attrs of diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py index 6d06db94c7..034f65a32f 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py @@ -2,7 +2,7 @@ Module for the dual-branch fall-back Draft->Published Versioning ModuleStore """ -from split import SplitMongoModuleStore, EXCLUDE_ALL +from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore, EXCLUDE_ALL from xmodule.exceptions import InvalidVersionError from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.exceptions import InsufficientSpecificationError @@ -13,7 +13,7 @@ from opaque_keys.edx.locator import CourseLocator from xmodule.modulestore.split_mongo import BlockKey -class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleStore): +class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPublished): """ A subclass of Split that supports a dual-branch fall-back versioning framework with a Draft branch that falls back to a Published branch. @@ -43,9 +43,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS # create any other necessary things as a side effect: ensure they populate the draft branch # and rely on auto publish to populate the published branch: split's create course doesn't # call super b/c it needs the auto publish above to have happened before any of the create_items - # in this. The explicit use of SplitMongoModuleStore is intentional + # in this; so, this manually calls the grandparent and above methods. with self.branch_setting(ModuleStoreEnum.Branch.draft_preferred, item.id): - # pylint: disable=bad-super-call + # NOTE: DO NOT CHANGE THE SUPER. See comment above super(SplitMongoModuleStore, self).create_course( org, course, run, user_id, runtime=item.runtime, **kwargs ) @@ -229,7 +229,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS if revision == ModuleStoreEnum.RevisionOption.draft_preferred: revision = ModuleStoreEnum.RevisionOption.draft_only location = self._map_revision_to_branch(location, revision=revision) - return SplitMongoModuleStore.get_parent_location(self, location, **kwargs) + return super(DraftVersioningModuleStore, self).get_parent_location(location, **kwargs) def get_orphans(self, course_key, **kwargs): course_key = self._map_revision_to_branch(course_key) @@ -275,8 +275,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS Publishes the subtree under location from the draft branch to the published branch Returns the newly published item. """ - SplitMongoModuleStore.copy( - self, + super(DraftVersioningModuleStore, self).copy( user_id, # Directly using the replace function rather than the for_branch function # because for_branch obliterates the version_guid and will lead to missed version conflicts. @@ -446,3 +445,62 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS if published_block is not None: setattr(xblock, '_published_by', published_block['edit_info']['edited_by']) setattr(xblock, '_published_on', published_block['edit_info']['edited_on']) + + def _find_asset_info(self, asset_key, thumbnail=False, **kwargs): + return super(DraftVersioningModuleStore, self)._find_asset_info( + self._map_revision_to_branch(asset_key), thumbnail, **kwargs + ) + + def _get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, get_thumbnails=False, **kwargs): + return super(DraftVersioningModuleStore, self)._get_all_asset_metadata( + self._map_revision_to_branch(course_key), start, maxresults, sort, get_thumbnails, **kwargs + ) + + def _update_course_assets(self, user_id, asset_key, update_function, get_thumbnail=False): + """ + Updates both the published and draft branches + """ + # if one call gets an exception, don't do the other call but pass on the exception + super(DraftVersioningModuleStore, self)._update_course_assets( + user_id, self._map_revision_to_branch(asset_key, ModuleStoreEnum.RevisionOption.published_only), + update_function, get_thumbnail + ) + super(DraftVersioningModuleStore, self)._update_course_assets( + user_id, self._map_revision_to_branch(asset_key, ModuleStoreEnum.RevisionOption.draft_only), + update_function, get_thumbnail + ) + + def _find_course_asset(self, course_key, filename, get_thumbnail=False): + return super(DraftVersioningModuleStore, self)._find_course_asset( + self._map_revision_to_branch(course_key), filename, get_thumbnail=get_thumbnail + ) + + def _find_course_assets(self, course_key): + """ + Split specific lookup + """ + return super(DraftVersioningModuleStore, self)._find_course_assets( + self._map_revision_to_branch(course_key) + ) + + def delete_all_asset_metadata(self, course_key, user_id): + """ + Deletes from both branches + """ + super(DraftVersioningModuleStore, self).delete_all_asset_metadata( + self._map_revision_to_branch(course_key, ModuleStoreEnum.RevisionOption.published_only), user_id + ) + super(DraftVersioningModuleStore, self).delete_all_asset_metadata( + self._map_revision_to_branch(course_key, ModuleStoreEnum.RevisionOption.draft_only), user_id + ) + + def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id): + """ + Copies to and from both branches + """ + for revision in [ModuleStoreEnum.RevisionOption.published_only, ModuleStoreEnum.RevisionOption.draft_only]: + super(DraftVersioningModuleStore, self).copy_all_asset_metadata( + self._map_revision_to_branch(source_course_key, revision), + self._map_revision_to_branch(dest_course_key, revision), + user_id + )