Commit db682dae by John Eskew

Merge pull request #5819 from edx/jeskew/asset_mongo_coursewide

Implement course-wide asset paging methods.
parents e8e7e78e 3534ba70
...@@ -5,6 +5,7 @@ Classes representing asset & asset thumbnail metadata. ...@@ -5,6 +5,7 @@ Classes representing asset & asset thumbnail metadata.
from datetime import datetime from datetime import datetime
import pytz import pytz
from contracts import contract, new_contract from contracts import contract, new_contract
from bisect import bisect_left, bisect_right
from opaque_keys.edx.keys import CourseKey, AssetKey from opaque_keys.edx.keys import CourseKey, AssetKey
new_contract('AssetKey', AssetKey) new_contract('AssetKey', AssetKey)
...@@ -33,8 +34,8 @@ class AssetMetadata(object): ...@@ -33,8 +34,8 @@ 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='basestring | None', internal_name='str | None', locked='bool | None', contenttype='basestring | None', @contract(asset_id='AssetKey', basename='basestring|None', internal_name='basestring|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') md5='basestring|None', curr_version='basestring|None', prev_version='basestring|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,
...@@ -99,15 +100,13 @@ class AssetMetadata(object): ...@@ -99,15 +100,13 @@ class AssetMetadata(object):
'locked': self.locked, 'locked': self.locked,
'contenttype': self.contenttype, 'contenttype': self.contenttype,
'md5': self.md5, 'md5': self.md5,
'edit_info': {
'curr_version': self.curr_version, 'curr_version': self.curr_version,
'prev_version': self.prev_version, 'prev_version': self.prev_version,
'edited_by': self.edited_by, 'edited_by': self.edited_by,
'edited_on': self.edited_on 'edited_on': self.edited_on
} }
}
@contract(asset_doc='dict | None') @contract(asset_doc='dict|None')
def from_mongo(self, asset_doc): def from_mongo(self, asset_doc):
""" """
Fill in all metadata fields from a MongoDB document. Fill in all metadata fields from a MongoDB document.
...@@ -121,11 +120,10 @@ class AssetMetadata(object): ...@@ -121,11 +120,10 @@ class AssetMetadata(object):
self.locked = asset_doc['locked'] self.locked = asset_doc['locked']
self.contenttype = asset_doc['contenttype'] self.contenttype = asset_doc['contenttype']
self.md5 = asset_doc['md5'] self.md5 = asset_doc['md5']
edit_info = asset_doc['edit_info'] self.curr_version = asset_doc['curr_version']
self.curr_version = edit_info['curr_version'] self.prev_version = asset_doc['prev_version']
self.prev_version = edit_info['prev_version'] self.edited_by = asset_doc['edited_by']
self.edited_by = edit_info['edited_by'] self.edited_on = asset_doc['edited_on']
self.edited_on = edit_info['edited_on']
class AssetThumbnailMetadata(object): class AssetThumbnailMetadata(object):
...@@ -136,7 +134,7 @@ class AssetThumbnailMetadata(object): ...@@ -136,7 +134,7 @@ class AssetThumbnailMetadata(object):
# All AssetThumbnailMetadata objects should have AssetLocators with this type. # All AssetThumbnailMetadata objects should have AssetLocators with this type.
ASSET_TYPE = 'thumbnail' ASSET_TYPE = 'thumbnail'
@contract(asset_id='AssetKey', internal_name='str | unicode | None') @contract(asset_id='AssetKey', internal_name='basestring|None')
def __init__(self, asset_id, internal_name=None, field_decorator=None): def __init__(self, asset_id, internal_name=None, field_decorator=None):
""" """
Construct a AssetThumbnailMetadata object. Construct a AssetThumbnailMetadata object.
...@@ -162,7 +160,7 @@ class AssetThumbnailMetadata(object): ...@@ -162,7 +160,7 @@ class AssetThumbnailMetadata(object):
'internal_name': self.internal_name 'internal_name': self.internal_name
} }
@contract(thumbnail_doc='dict | None') @contract(thumbnail_doc='dict|None')
def from_mongo(self, thumbnail_doc): def from_mongo(self, thumbnail_doc):
""" """
Fill in all metadata fields from a MongoDB document. Fill in all metadata fields from a MongoDB document.
......
...@@ -14,6 +14,8 @@ import collections ...@@ -14,6 +14,8 @@ import collections
from contextlib import contextmanager from contextlib import contextmanager
import functools import functools
import threading import threading
from operator import itemgetter
from sortedcontainers import SortedListWithKey
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from contracts import contract, new_contract from contracts import contract, new_contract
...@@ -98,6 +100,13 @@ class ModuleStoreEnum(object): ...@@ -98,6 +100,13 @@ class ModuleStoreEnum(object):
# user ID to use for tests that do not have a django user available # user ID to use for tests that do not have a django user available
test = -3 test = -3
class SortOrder(object):
"""
Values for sorting asset metadata.
"""
ascending = 1
descending = 2
class BulkOpsRecord(object): class BulkOpsRecord(object):
""" """
...@@ -292,19 +301,23 @@ class ModuleStoreAssetInterface(object): ...@@ -292,19 +301,23 @@ class ModuleStoreAssetInterface(object):
if course_assets is None: if course_assets is None:
return None, None return None, None
if get_thumbnail: info = 'thumbnails' if get_thumbnail else 'assets'
all_assets = course_assets['thumbnails'] all_assets = SortedListWithKey([], key=itemgetter('filename'))
else: # Assets should be pre-sorted, so add them efficiently without sorting.
all_assets = course_assets['assets'] # extend() will raise a ValueError if the passed-in list is not sorted.
all_assets.extend(course_assets.get(info, []))
# See if this asset already exists by checking the external_filename. # See if this asset already exists by checking the external_filename.
# Studio doesn't currently support using multiple course assets with the same filename. # Studio doesn't currently support using multiple course assets with the same filename.
# So use the filename as the unique identifier. # So use the filename as the unique identifier.
for idx, asset in enumerate(all_assets): idx = None
if asset['filename'] == filename: idx_left = all_assets.bisect_left({'filename': filename})
return course_assets, idx idx_right = all_assets.bisect_right({'filename': filename})
if idx_left != idx_right:
# Asset was found in the list.
idx = idx_left
return course_assets, None return course_assets, idx
@contract(asset_key='AssetKey') @contract(asset_key='AssetKey')
def _find_asset_info(self, asset_key, thumbnail=False, **kwargs): def _find_asset_info(self, asset_key, thumbnail=False, **kwargs):
...@@ -358,19 +371,19 @@ class ModuleStoreAssetInterface(object): ...@@ -358,19 +371,19 @@ class ModuleStoreAssetInterface(object):
""" """
return self._find_asset_info(asset_key, thumbnail=True, **kwargs) 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',
def _get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, get_thumbnails=False, **kwargs): sort='tuple(str,(int,>=1,<=2))|None', get_thumbnails='bool')
def _get_all_asset_metadata(self, course_key, start=0, maxresults=-1,
sort=('displayname', ModuleStoreEnum.SortOrder.ascending),
get_thumbnails=False, **kwargs):
""" """
Returns a list of static asset (or thumbnail) metadata for a course.
Args:
course_key (CourseKey): course identifier course_key (CourseKey): course identifier
start (int): optional - start at this asset number start (int): optional - start at this asset number. Zero-based!
maxresults (int): optional - return at most this many, -1 means no limit maxresults (int): optional - return at most this many, -1 means no limit
sort (array): optional - None means no sort sort (array): optional - None means no sort
(sort_by (str), sort_order (str)) (sort_by (str), sort_order (str))
sort_by - one of 'uploadDate' or 'displayname' sort_by - one of 'uploadDate' or 'displayname'
sort_order - one of 'ascending' or 'descending' sort_order - one of SortOrder.ascending or SortOrder.descending
get_thumbnails (bool): True if getting thumbnail metadata, else getting asset metadata get_thumbnails (bool): True if getting thumbnail metadata, else getting asset metadata
Returns: Returns:
...@@ -382,35 +395,60 @@ class ModuleStoreAssetInterface(object): ...@@ -382,35 +395,60 @@ class ModuleStoreAssetInterface(object):
# to distinguish zero assets from "not able to retrieve assets". # to distinguish zero assets from "not able to retrieve assets".
return None return None
if get_thumbnails: # Determine the proper sort - with defaults of ('displayname', SortOrder.ascending).
all_assets = course_assets.get('thumbnails', []) sort_field = 'filename'
else: sort_order = ModuleStoreEnum.SortOrder.ascending
all_assets = course_assets.get('assets', []) if sort:
if sort[0] == 'uploadDate' and not get_thumbnails:
# DO_NEXT: Add start/maxresults/sort functionality as part of https://openedx.atlassian.net/browse/PLAT-74 sort_field = 'edited_on'
if start and maxresults and sort: if sort[1] == ModuleStoreEnum.SortOrder.descending:
pass sort_order = ModuleStoreEnum.SortOrder.descending
info = 'thumbnails' if get_thumbnails else 'assets'
all_assets = SortedListWithKey(course_assets.get(info, []), key=itemgetter(sort_field))
num_assets = len(all_assets)
start_idx = start
end_idx = min(num_assets, start + maxresults)
if maxresults < 0:
# No limit on the results.
end_idx = num_assets
step_incr = 1
if sort_order == ModuleStoreEnum.SortOrder.descending:
# Flip the indices and iterate backwards.
step_incr = -1
start_idx = (num_assets - 1) - start_idx
end_idx = (num_assets - 1) - end_idx
ret_assets = [] ret_assets = []
for asset in all_assets: for idx in xrange(start_idx, end_idx, step_incr):
asset = all_assets[idx]
if get_thumbnails: if get_thumbnails:
thumb = AssetThumbnailMetadata( thumb = AssetThumbnailMetadata(
course_key.make_asset_key('thumbnail', asset['filename']), course_key.make_asset_key('thumbnail', asset['filename']),
internal_name=asset['filename'], **kwargs internal_name=asset['filename'],
**kwargs
) )
ret_assets.append(thumb) ret_assets.append(thumb)
else: else:
asset = AssetMetadata( new_asset = AssetMetadata(
course_key.make_asset_key('asset', asset['filename']), course_key.make_asset_key('asset', asset['filename']),
basename=asset['filename'], basename=asset['filename'],
edited_on=asset['edit_info']['edited_on'], internal_name=asset['internal_name'],
locked=asset['locked'],
contenttype=asset['contenttype'], contenttype=asset['contenttype'],
md5=str(asset['md5']), **kwargs md5=asset['md5'],
curr_version=asset['curr_version'],
prev_version=asset['prev_version'],
edited_on=asset['edited_on'],
edited_by=asset['edited_by'],
**kwargs
) )
ret_assets.append(asset) ret_assets.append(new_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='tuple(str,int)|None')
def get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, **kwargs): 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.
...@@ -423,7 +461,7 @@ class ModuleStoreAssetInterface(object): ...@@ -423,7 +461,7 @@ class ModuleStoreAssetInterface(object):
sort (array): optional - None means no sort sort (array): optional - None means no sort
(sort_by (str), sort_order (str)) (sort_by (str), sort_order (str))
sort_by - one of 'uploadDate' or 'displayname' sort_by - one of 'uploadDate' or 'displayname'
sort_order - one of 'ascending' or 'descending' sort_order - one of SortOrder.ascending or SortOrder.descending
Returns: Returns:
List of AssetMetadata objects. List of AssetMetadata objects.
......
...@@ -370,7 +370,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase): ...@@ -370,7 +370,7 @@ class MixedModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase):
return store.find_asset_thumbnail_metadata(asset_key, **kwargs) return store.find_asset_thumbnail_metadata(asset_key, **kwargs)
@strip_key @strip_key
@contract(course_key='CourseKey', start=int, maxresults=int, sort='list | None') @contract(course_key='CourseKey', start=int, maxresults=int, sort='tuple|None')
def get_all_asset_metadata(self, course_key, start=0, maxresults=-1, sort=None, **kwargs): 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.
......
...@@ -25,6 +25,8 @@ from path import path ...@@ -25,6 +25,8 @@ from path import path
from datetime import datetime from datetime import datetime
from pytz import UTC from pytz import UTC
from contracts import contract, new_contract from contracts import contract, new_contract
from operator import itemgetter
from sortedcontainers import SortedListWithKey
from importlib import import_module from importlib import import_module
from xmodule.errortracker import null_error_tracker, exc_info_to_str from xmodule.errortracker import null_error_tracker, exc_info_to_str
...@@ -1493,7 +1495,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1493,7 +1495,10 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
course_assets, asset_idx = self._find_course_asset(course_key, asset_metadata.asset_id.path, thumbnail) course_assets, asset_idx = self._find_course_asset(course_key, asset_metadata.asset_id.path, thumbnail)
info = 'thumbnails' if thumbnail else 'assets' info = 'thumbnails' if thumbnail else 'assets'
all_assets = course_assets[info] all_assets = SortedListWithKey([], key=itemgetter('filename'))
# Assets should be pre-sorted, so add them efficiently without sorting.
# extend() will raise a ValueError if the passed-in list is not sorted.
all_assets.extend(course_assets[info])
# Set the edited information for assets only - not thumbnails. # Set the edited information for assets only - not thumbnails.
if not thumbnail: if not thumbnail:
...@@ -1502,17 +1507,38 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo ...@@ -1502,17 +1507,38 @@ class MongoModuleStore(ModuleStoreDraftAndPublished, ModuleStoreWriteBase, Mongo
# Translate metadata to Mongo format. # Translate metadata to Mongo format.
metadata_to_insert = asset_metadata.to_mongo() metadata_to_insert = asset_metadata.to_mongo()
if asset_idx is None: if asset_idx is None:
# Append new metadata. # Add new metadata sorted into the list.
# Future optimization: Insert in order & binary search to retrieve. all_assets.add(metadata_to_insert)
all_assets.append(metadata_to_insert)
else: else:
# Replace existing metadata. # Replace existing metadata.
all_assets[asset_idx] = metadata_to_insert all_assets[asset_idx] = metadata_to_insert
# Update the document. # Update the document.
self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_assets}}) self.asset_collection.update({'_id': course_assets['_id']}, {'$set': {info: all_assets.as_list()}})
return True return True
@contract(source_course_key='CourseKey', dest_course_key='CourseKey')
def copy_all_asset_metadata(self, source_course_key, dest_course_key, user_id):
"""
Copy all the course assets from source_course_key to dest_course_key.
Arguments:
source_course_key (CourseKey): identifier of course to copy from
dest_course_key (CourseKey): identifier of course to copy to
"""
source_assets = self._find_course_assets(source_course_key)
dest_assets = self._find_course_assets(dest_course_key)
dest_assets['assets'] = source_assets.get('assets', [])
dest_assets['thumbnails'] = source_assets.get('thumbnails', [])
# Update the document.
self.asset_collection.update(
{'_id': dest_assets['_id']},
{'$set': {'assets': dest_assets['assets'],
'thumbnails': dest_assets['thumbnails']}
}
)
@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):
""" """
......
...@@ -76,6 +76,7 @@ scipy==0.14.0 ...@@ -76,6 +76,7 @@ scipy==0.14.0
Shapely==1.2.16 Shapely==1.2.16
singledispatch==3.4.0.2 singledispatch==3.4.0.2
sorl-thumbnail==11.12 sorl-thumbnail==11.12
sortedcontainers==0.9.2
South==0.7.6 South==0.7.6
stevedore==0.14.1 stevedore==0.14.1
sure==1.2.3 sure==1.2.3
......
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