Commit b6fac239 by Don Mitchell

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
parent 0064d29d
...@@ -3,11 +3,13 @@ Classes representing asset & asset thumbnail metadata. ...@@ -3,11 +3,13 @@ Classes representing asset & asset thumbnail metadata.
""" """
from datetime import datetime from datetime import datetime
import pytz
from contracts import contract, new_contract from contracts import contract, new_contract
from opaque_keys.edx.keys import CourseKey, AssetKey from opaque_keys.edx.keys import CourseKey, AssetKey
new_contract('AssetKey', AssetKey) new_contract('AssetKey', AssetKey)
new_contract('datetime', datetime) new_contract('datetime', datetime)
new_contract('basestring', basestring)
class IncorrectAssetIdType(Exception): class IncorrectAssetIdType(Exception):
...@@ -31,12 +33,13 @@ class AssetMetadata(object): ...@@ -31,12 +33,13 @@ class AssetMetadata(object):
# All AssetMetadata objects should have AssetLocators with this type. # All AssetMetadata objects should have AssetLocators with this type.
ASSET_TYPE = 'asset' ASSET_TYPE = 'asset'
@contract(asset_id='AssetKey', basename='str | unicode | None', internal_name='str | None', locked='bool | None', @contract(asset_id='AssetKey', basename='basestring | None', internal_name='str | None', locked='bool | None', contenttype='basestring | None',
contenttype='str | unicode | None', md5='str | None', curr_version='str | None', prev_version='str | 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, def __init__(self, asset_id,
basename=None, internal_name=None, basename=None, internal_name=None,
locked=None, contenttype=None, md5=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. Construct a AssetMetadata object.
...@@ -48,10 +51,13 @@ class AssetMetadata(object): ...@@ -48,10 +51,13 @@ class AssetMetadata(object):
contenttype (str): MIME type of the asset. contenttype (str): MIME type of the asset.
curr_version (str): Current version of the asset. curr_version (str): Current version of the asset.
prev_version (str): Previous 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: if asset_id.asset_type != self.ASSET_TYPE:
raise IncorrectAssetIdType() 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.basename = basename # Path w/o filename.
self.internal_name = internal_name self.internal_name = internal_name
self.locked = locked self.locked = locked
...@@ -59,8 +65,8 @@ class AssetMetadata(object): ...@@ -59,8 +65,8 @@ class AssetMetadata(object):
self.md5 = md5 self.md5 = md5
self.curr_version = curr_version self.curr_version = curr_version
self.prev_version = prev_version self.prev_version = prev_version
self.edited_by = None self.edited_by = edited_by
self.edited_on = None self.edited_on = edited_on or datetime.now(pytz.utc)
def __repr__(self): def __repr__(self):
return """AssetMetadata{!r}""".format(( return """AssetMetadata{!r}""".format((
...@@ -131,7 +137,7 @@ class AssetThumbnailMetadata(object): ...@@ -131,7 +137,7 @@ class AssetThumbnailMetadata(object):
ASSET_TYPE = 'thumbnail' ASSET_TYPE = 'thumbnail'
@contract(asset_id='AssetKey', internal_name='str | unicode | None') @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. Construct a AssetThumbnailMetadata object.
...@@ -141,7 +147,7 @@ class AssetThumbnailMetadata(object): ...@@ -141,7 +147,7 @@ class AssetThumbnailMetadata(object):
""" """
if asset_id.asset_type != self.ASSET_TYPE: if asset_id.asset_type != self.ASSET_TYPE:
raise IncorrectAssetIdType() 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 self.internal_name = internal_name
def __repr__(self): def __repr__(self):
......
...@@ -913,7 +913,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -913,7 +913,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
""" """
raise NotImplementedError() 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): def save_asset_metadata(self, course_key, asset_metadata, user_id):
""" """
Saves the asset metadata for a particular course's asset. Saves the asset metadata for a particular course's asset.
...@@ -928,7 +928,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -928,7 +928,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
return self._save_asset_info(course_key, asset_metadata, user_id, thumbnail=False) return self._save_asset_info(course_key, asset_metadata, user_id, thumbnail=False)
@contract(course_key='CourseKey', asset_thumbnail_metadata='AssetThumbnailMetadata') @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. Saves the asset thumbnail metadata for a particular course asset's thumbnail.
...@@ -939,10 +939,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -939,10 +939,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
True if thumbnail metadata save was successful, else False 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') @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. Find the info for a particular course asset/thumbnail.
...@@ -959,16 +959,16 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -959,16 +959,16 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
if thumbnail: if thumbnail:
info = 'thumbnails' info = 'thumbnails'
mdata = AssetThumbnailMetadata(asset_key, asset_key.path) mdata = AssetThumbnailMetadata(asset_key, asset_key.path, **kwargs)
else: else:
info = 'assets' info = 'assets'
mdata = AssetMetadata(asset_key, asset_key.path) mdata = AssetMetadata(asset_key, asset_key.path, **kwargs)
all_assets = course_assets[info] all_assets = course_assets[info]
mdata.from_mongo(all_assets[asset_idx]) mdata.from_mongo(all_assets[asset_idx])
return mdata return mdata
@contract(asset_key='AssetKey') @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. Find the metadata for a particular course asset.
...@@ -978,10 +978,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -978,10 +978,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
asset metadata (AssetMetadata) -or- None if not found 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') @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. Find the metadata for a particular course asset.
...@@ -991,10 +991,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -991,10 +991,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
asset metadata (AssetMetadata) -or- None if not found 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') @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. Returns a list of static asset (or thumbnail) metadata for a course.
...@@ -1018,9 +1018,9 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1018,9 +1018,9 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
return None return None
if get_thumbnails: if get_thumbnails:
all_assets = course_assets['thumbnails'] all_assets = course_assets.get('thumbnails', [])
else: 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 # DO_NEXT: Add start/maxresults/sort functionality as part of https://openedx.atlassian.net/browse/PLAT-74
if start and maxresults and sort: if start and maxresults and sort:
...@@ -1029,17 +1029,24 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1029,17 +1029,24 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
ret_assets = [] ret_assets = []
for asset in all_assets: for asset in all_assets:
if get_thumbnails: if get_thumbnails:
thumb = AssetThumbnailMetadata(course_key.make_asset_key('thumbnail', asset['filename']), thumb = AssetThumbnailMetadata(
internal_name=asset['filename']) course_key.make_asset_key('thumbnail', asset['filename']),
internal_name=asset['filename'], **kwargs
)
ret_assets.append(thumb) ret_assets.append(thumb)
else: else:
one_asset = AssetMetadata(course_key.make_asset_key('asset', asset['filename'])) asset = AssetMetadata(
one_asset.from_mongo(asset) course_key.make_asset_key('asset', asset['filename']),
ret_assets.append(one_asset) 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 return ret_assets
@contract(course_key='CourseKey', start='int | None', maxresults='int | None', sort='list | None') @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. 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. 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): ...@@ -1056,10 +1063,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
List of AssetMetadata objects. 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') @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. Returns a list of thumbnails for all course assets.
...@@ -1069,7 +1076,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1069,7 +1076,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
List of AssetThumbnailMetadata objects. 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): def set_asset_metadata_attrs(self, asset_key, attrs, user_id):
""" """
...@@ -1077,7 +1084,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1077,7 +1084,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
""" """
raise NotImplementedError() 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. Base method to over-ride in modulestore.
""" """
...@@ -1100,7 +1107,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1100,7 +1107,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
return self.set_asset_metadata_attrs(asset_key, {attr: value}, user_id) return self.set_asset_metadata_attrs(asset_key, {attr: value}, user_id)
@contract(asset_key='AssetKey') @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. Deletes a single asset's metadata.
...@@ -1110,10 +1117,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1110,10 +1117,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
Number of asset metadata entries deleted (0 or 1) 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') @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. Deletes a single asset's metadata.
...@@ -1123,10 +1130,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite): ...@@ -1123,10 +1130,10 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
Returns: Returns:
Number of asset metadata entries deleted (0 or 1) 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') @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. Copy all the course assets from source_course_key to dest_course_key.
......
...@@ -315,24 +315,6 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -315,24 +315,6 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
store = self._get_modulestore_for_courseid(course_key) store = self._get_modulestore_for_courseid(course_key)
return store.delete_course(course_key, user_id) 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') @contract(course_key='CourseKey', asset_metadata='AssetMetadata')
def save_asset_metadata(self, course_key, asset_metadata, user_id): def save_asset_metadata(self, course_key, asset_metadata, user_id):
""" """
...@@ -341,30 +323,25 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -341,30 +323,25 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Args: Args:
course_key (CourseKey): course identifier course_key (CourseKey): course identifier
asset_metadata (AssetMetadata): data about the course asset data 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) store = self._get_modulestore_for_courseid(course_key)
return store.save_asset_metadata(course_key, asset_metadata, user_id) return store.save_asset_metadata(course_key, asset_metadata, user_id)
@contract(course_key='CourseKey', asset_thumbnail_metadata='AssetThumbnailMetadata') @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. Saves the asset thumbnail metadata for a particular course asset's thumbnail.
Arguments: Arguments:
course_key (CourseKey): course identifier course_key (CourseKey): course identifier
asset_thumbnail_metadata (AssetThumbnailMetadata): data about the course asset thumbnail 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) 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') @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. Find the metadata for a particular course asset.
...@@ -375,10 +352,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -375,10 +352,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
asset metadata (AssetMetadata) -or- None if not found asset metadata (AssetMetadata) -or- None if not found
""" """
store = self._get_modulestore_for_courseid(asset_key.course_key) 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') @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. Find the metadata for a particular course asset.
...@@ -389,10 +367,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -389,10 +367,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
asset metadata (AssetMetadata) -or- None if not found asset metadata (AssetMetadata) -or- None if not found
""" """
store = self._get_modulestore_for_courseid(asset_key.course_key) 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') @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. 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. 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): ...@@ -415,10 +394,11 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
md5: An md5 hash of the asset content md5: An md5 hash of the asset content
""" """
store = self._get_modulestore_for_courseid(course_key) 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') @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. Returns a list of thumbnails for all course assets.
...@@ -429,10 +409,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -429,10 +409,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
List of AssetThumbnailMetadata objects. List of AssetThumbnailMetadata objects.
""" """
store = self._get_modulestore_for_courseid(course_key) 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') @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. Deletes a single asset's metadata.
...@@ -443,10 +423,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -443,10 +423,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Number of asset metadata entries deleted (0 or 1) Number of asset metadata entries deleted (0 or 1)
""" """
store = self._get_modulestore_for_courseid(asset_key.course_key) 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') @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. Deletes a single asset's metadata.
...@@ -457,10 +437,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -457,10 +437,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
Number of asset metadata entries deleted (0 or 1) Number of asset metadata entries deleted (0 or 1)
""" """
store = self._get_modulestore_for_courseid(asset_key.course_key) 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') @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. Delete all of the assets which use this course_key as an identifier.
...@@ -468,10 +448,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -468,10 +448,10 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
course_key (CourseKey): course_identifier course_key (CourseKey): course_identifier
""" """
store = self._get_modulestore_for_courseid(course_key) 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') @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. Copy all the course assets from source_course_key to dest_course_key.
...@@ -483,7 +463,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -483,7 +463,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
# Check the modulestores of both the source and dest course_keys. If in different modulestores, # 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. # export all asset data from one modulestore and import it into the dest one.
store = self._get_modulestore_for_courseid(source_course_key) 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) @contract(asset_key='AssetKey', attr=str)
def set_asset_metadata_attr(self, asset_key, attr, value, user_id): def set_asset_metadata_attr(self, asset_key, attr, value, user_id):
...@@ -500,7 +480,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -500,7 +480,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
AttributeError is attr is one of the build in attrs. AttributeError is attr is one of the build in attrs.
""" """
store = self._get_modulestore_for_courseid(asset_key.course_key) 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) @contract(asset_key='AssetKey', attr_dict=dict)
def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id): def set_asset_metadata_attrs(self, asset_key, attr_dict, user_id):
......
...@@ -1474,7 +1474,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1474,7 +1474,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
return course_assets 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): def _save_asset_info(self, course_key, asset_metadata, user_id, thumbnail=False):
""" """
Saves the info for a particular course's asset/thumbnail. Saves the info for a particular course's asset/thumbnail.
...@@ -1537,7 +1537,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1537,7 +1537,6 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
md = AssetMetadata(asset_key, asset_key.path) md = AssetMetadata(asset_key, asset_key.path)
md.from_mongo(all_assets[asset_idx]) md.from_mongo(all_assets[asset_idx])
md.update(attr_dict) 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. # Generate a Mongo doc from the metadata and update the course asset info.
all_assets[asset_idx] = md.to_mongo() all_assets[asset_idx] = md.to_mongo()
...@@ -1545,7 +1544,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1545,7 +1544,7 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
self.asset_collection.update({'_id': course_assets['_id']}, {"$set": {'assets': all_assets}}) self.asset_collection.update({'_id': course_assets['_id']}, {"$set": {'assets': all_assets}})
@contract(asset_key='AssetKey') @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. Internal; deletes a single asset's metadata -or- thumbnail.
...@@ -1572,8 +1571,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1572,8 +1571,9 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_asset_info}}) self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_asset_info}})
return 1 return 1
# pylint: disable=unused-argument
@contract(course_key='CourseKey') @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. 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 ...@@ -80,6 +80,7 @@ from xmodule.modulestore.split_mongo import BlockKey, CourseEnvelope
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
from collections import defaultdict from collections import defaultdict
from types import NoneType from types import NoneType
from xmodule.assetstore import AssetMetadata
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -1174,7 +1175,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -1174,7 +1175,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
Find the version_history_depth next versions of this definition. Return as a VersionTree Find the version_history_depth next versions of this definition. Return as a VersionTree
''' '''
# TODO implement # TODO implement
raise NotImplementedError() pass
def create_definition_from_data(self, course_key, new_def_data, category, user_id): def create_definition_from_data(self, course_key, new_def_data, category, user_id):
""" """
...@@ -2120,6 +2121,180 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -2120,6 +2121,180 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
""" """
return ModuleStoreEnum.Type.split 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): def internal_clean_children(self, course_locator):
""" """
Only intended for rather low level methods to use. Goes through the children attrs of Only intended for rather low level methods to use. Goes through the children attrs of
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
Module for the dual-branch fall-back Draft->Published Versioning ModuleStore 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.exceptions import InvalidVersionError
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.exceptions import InsufficientSpecificationError from xmodule.modulestore.exceptions import InsufficientSpecificationError
...@@ -13,7 +13,7 @@ from opaque_keys.edx.locator import CourseLocator ...@@ -13,7 +13,7 @@ from opaque_keys.edx.locator import CourseLocator
from xmodule.modulestore.split_mongo import BlockKey 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 A subclass of Split that supports a dual-branch fall-back versioning framework
with a Draft branch that falls back to a Published branch. with a Draft branch that falls back to a Published branch.
...@@ -43,9 +43,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -43,9 +43,9 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
# create any other necessary things as a side effect: ensure they populate the draft branch # 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 # 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 # 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): 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( super(SplitMongoModuleStore, self).create_course(
org, course, run, user_id, runtime=item.runtime, **kwargs org, course, run, user_id, runtime=item.runtime, **kwargs
) )
...@@ -229,7 +229,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -229,7 +229,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
if revision == ModuleStoreEnum.RevisionOption.draft_preferred: if revision == ModuleStoreEnum.RevisionOption.draft_preferred:
revision = ModuleStoreEnum.RevisionOption.draft_only revision = ModuleStoreEnum.RevisionOption.draft_only
location = self._map_revision_to_branch(location, revision=revision) 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): def get_orphans(self, course_key, **kwargs):
course_key = self._map_revision_to_branch(course_key) course_key = self._map_revision_to_branch(course_key)
...@@ -275,8 +275,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -275,8 +275,7 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
Publishes the subtree under location from the draft branch to the published branch Publishes the subtree under location from the draft branch to the published branch
Returns the newly published item. Returns the newly published item.
""" """
SplitMongoModuleStore.copy( super(DraftVersioningModuleStore, self).copy(
self,
user_id, user_id,
# Directly using the replace function rather than the for_branch function # 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. # because for_branch obliterates the version_guid and will lead to missed version conflicts.
...@@ -446,3 +445,62 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS ...@@ -446,3 +445,62 @@ class DraftVersioningModuleStore(ModuleStoreDraftAndPublished, SplitMongoModuleS
if published_block is not None: if published_block is not None:
setattr(xblock, '_published_by', published_block['edit_info']['edited_by']) setattr(xblock, '_published_by', published_block['edit_info']['edited_by'])
setattr(xblock, '_published_on', published_block['edit_info']['edited_on']) 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
)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment