Implement asset metadata storage in Split - per-asset functionality
PLAT-40 Conflicts: common/lib/xmodule/xmodule/assetstore/__init__.py common/lib/xmodule/xmodule/modulestore/__init__.py common/lib/xmodule/xmodule/modulestore/mixed.py common/lib/xmodule/xmodule/modulestore/mongo/base.py
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user