Merge pull request #16006 from edx/mroytman/EDU-1165
refactor and add multiple filter functionality to assets.py
This commit is contained in:
@@ -28,17 +28,22 @@ from util.json_request import JsonResponse
|
||||
|
||||
__all__ = ['assets_handler']
|
||||
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
REQUEST_DEFAULTS = {
|
||||
'page': 0,
|
||||
'page_size': 50,
|
||||
'sort': 'date_added',
|
||||
'direction': '',
|
||||
'asset_type': ''
|
||||
}
|
||||
|
||||
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def assets_handler(request, course_key_string=None, asset_key_string=None):
|
||||
"""
|
||||
'''
|
||||
The restful handler for assets.
|
||||
It allows retrieval of all the assets (as an HTML page), as well as uploading new assets,
|
||||
deleting assets, and changing the "locked" state of an asset.
|
||||
deleting assets, and changing the 'locked' state of an asset.
|
||||
|
||||
GET
|
||||
html: return an html page which will show all course assets. Note that only the asset container
|
||||
@@ -46,38 +51,47 @@ def assets_handler(request, course_key_string=None, asset_key_string=None):
|
||||
json: returns a page of assets. The following parameters are supported:
|
||||
page: the desired page of results (defaults to 0)
|
||||
page_size: the number of items per page (defaults to 50)
|
||||
sort: the asset field to sort by (defaults to "date_added")
|
||||
direction: the sort direction (defaults to "descending")
|
||||
sort: the asset field to sort by (defaults to 'date_added')
|
||||
direction: the sort direction (defaults to 'descending')
|
||||
POST
|
||||
json: create (or update?) an asset. The only updating that can be done is changing the lock state.
|
||||
PUT
|
||||
json: update the locked state of an asset
|
||||
DELETE
|
||||
json: delete an asset
|
||||
"""
|
||||
'''
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
if not has_course_author_access(request.user, course_key):
|
||||
raise PermissionDenied()
|
||||
|
||||
response_format = request.GET.get('format') or request.POST.get('format') or 'html'
|
||||
if response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json'):
|
||||
response_format = _get_response_format(request)
|
||||
if _request_response_format_is_json(request, response_format):
|
||||
if request.method == 'GET':
|
||||
return _assets_json(request, course_key)
|
||||
else:
|
||||
asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None
|
||||
return _update_asset(request, course_key, asset_key)
|
||||
|
||||
asset_key = AssetKey.from_string(asset_key_string) if asset_key_string else None
|
||||
return _update_asset(request, course_key, asset_key)
|
||||
|
||||
elif request.method == 'GET': # assume html
|
||||
return _asset_index(request, course_key)
|
||||
else:
|
||||
return HttpResponseNotFound()
|
||||
return _asset_index(course_key)
|
||||
|
||||
return HttpResponseNotFound()
|
||||
|
||||
|
||||
def _asset_index(request, course_key):
|
||||
"""
|
||||
def _get_response_format(request):
|
||||
return request.GET.get('format') or request.POST.get('format') or 'html'
|
||||
|
||||
|
||||
def _request_response_format_is_json(request, response_format):
|
||||
return response_format == 'json' or 'application/json' in request.META.get('HTTP_ACCEPT', 'application/json')
|
||||
|
||||
|
||||
def _asset_index(course_key):
|
||||
'''
|
||||
Display an editable asset library.
|
||||
|
||||
Supports start (0-based index into the list of assets) and max query parameters.
|
||||
"""
|
||||
'''
|
||||
course_module = modulestore().get_course(course_key)
|
||||
|
||||
return render_to_response('asset_index.html', {
|
||||
@@ -90,102 +104,202 @@ def _asset_index(request, course_key):
|
||||
|
||||
|
||||
def _assets_json(request, course_key):
|
||||
"""
|
||||
'''
|
||||
Display an editable asset library.
|
||||
|
||||
Supports start (0-based index into the list of assets) and max query parameters.
|
||||
"""
|
||||
requested_page = int(request.GET.get('page', 0))
|
||||
requested_page_size = int(request.GET.get('page_size', 50))
|
||||
requested_sort = request.GET.get('sort', 'date_added')
|
||||
requested_filter = request.GET.get('asset_type', '')
|
||||
requested_file_types = settings.FILES_AND_UPLOAD_TYPE_FILTERS.get(
|
||||
requested_filter, None)
|
||||
filter_params = None
|
||||
if requested_filter:
|
||||
if requested_filter == 'OTHER':
|
||||
all_filters = settings.FILES_AND_UPLOAD_TYPE_FILTERS
|
||||
where = []
|
||||
for all_filter in all_filters:
|
||||
extension_filters = all_filters[all_filter]
|
||||
where.extend(
|
||||
["JSON.stringify(this.contentType).toUpperCase() != JSON.stringify('{}').toUpperCase()".format(
|
||||
extension_filter) for extension_filter in extension_filters])
|
||||
filter_params = {
|
||||
"$where": ' && '.join(where),
|
||||
}
|
||||
else:
|
||||
where = ["JSON.stringify(this.contentType).toUpperCase() == JSON.stringify('{}').toUpperCase()".format(
|
||||
req_filter) for req_filter in requested_file_types]
|
||||
filter_params = {
|
||||
"$where": ' || '.join(where),
|
||||
}
|
||||
'''
|
||||
request_options = _parse_request_to_dictionary(request)
|
||||
|
||||
sort_direction = DESCENDING
|
||||
if request.GET.get('direction', '').lower() == 'asc':
|
||||
sort_direction = ASCENDING
|
||||
filter_parameters = None
|
||||
|
||||
# Convert the field name to the Mongo name
|
||||
if requested_sort == 'date_added':
|
||||
requested_sort = 'uploadDate'
|
||||
elif requested_sort == 'display_name':
|
||||
requested_sort = 'displayname'
|
||||
sort = [(requested_sort, sort_direction)]
|
||||
if request_options['requested_asset_type']:
|
||||
filters_are_invalid_error = _get_error_if_invalid_parameters(request_options['requested_asset_type'])
|
||||
|
||||
current_page = max(requested_page, 0)
|
||||
start = current_page * requested_page_size
|
||||
options = {
|
||||
if filters_are_invalid_error is not None:
|
||||
return filters_are_invalid_error
|
||||
|
||||
filter_parameters = _get_filter_parameters_for_mongo(request_options['requested_asset_type'])
|
||||
|
||||
sort_type_and_direction = _get_sort_type_and_direction(request_options)
|
||||
|
||||
requested_page_size = request_options['requested_page_size']
|
||||
current_page = _get_current_page(request_options['requested_page'])
|
||||
first_asset_to_display_index = _get_first_asset_index(current_page, requested_page_size)
|
||||
|
||||
query_options = {
|
||||
'current_page': current_page,
|
||||
'page_size': requested_page_size,
|
||||
'sort': sort,
|
||||
'filter_params': filter_params
|
||||
'sort': sort_type_and_direction,
|
||||
'filter_params': filter_parameters
|
||||
}
|
||||
assets, total_count = _get_assets_for_page(request, course_key, options)
|
||||
end = start + len(assets)
|
||||
|
||||
# If the query is beyond the final page, then re-query the final page so
|
||||
# that at least one asset is returned
|
||||
if requested_page > 0 and start >= total_count:
|
||||
options['current_page'] = current_page = int(math.floor((total_count - 1) / requested_page_size))
|
||||
start = current_page * requested_page_size
|
||||
assets, total_count = _get_assets_for_page(request, course_key, options)
|
||||
end = start + len(assets)
|
||||
assets, total_count = _get_assets_for_page(course_key, query_options)
|
||||
|
||||
asset_json = []
|
||||
for asset in assets:
|
||||
asset_location = asset['asset_key']
|
||||
# note, due to the schema change we may not have a 'thumbnail_location'
|
||||
# in the result set
|
||||
thumbnail_location = asset.get('thumbnail_location', None)
|
||||
if thumbnail_location:
|
||||
thumbnail_location = course_key.make_asset_key(
|
||||
'thumbnail', thumbnail_location[4])
|
||||
if request_options['requested_page'] > 0 and first_asset_to_display_index >= total_count:
|
||||
_update_options_to_requery_final_page(query_options, total_count)
|
||||
current_page = query_options['current_page']
|
||||
first_asset_to_display_index = _get_first_asset_index(current_page, requested_page_size)
|
||||
assets, total_count = _get_assets_for_page(course_key, query_options)
|
||||
|
||||
asset_locked = asset.get('locked', False)
|
||||
asset_json.append(_get_asset_json(
|
||||
asset['displayname'],
|
||||
asset['contentType'],
|
||||
asset['uploadDate'],
|
||||
asset_location,
|
||||
thumbnail_location,
|
||||
asset_locked
|
||||
))
|
||||
last_asset_to_display_index = first_asset_to_display_index + len(assets)
|
||||
assets_in_json_format = _get_assets_in_json_format(assets, course_key)
|
||||
|
||||
return JsonResponse({
|
||||
'start': start,
|
||||
'end': end,
|
||||
response_payload = {
|
||||
'start': first_asset_to_display_index,
|
||||
'end': last_asset_to_display_index,
|
||||
'page': current_page,
|
||||
'pageSize': requested_page_size,
|
||||
'totalCount': total_count,
|
||||
'assets': asset_json,
|
||||
'sort': requested_sort,
|
||||
})
|
||||
'assets': assets_in_json_format,
|
||||
'sort': request_options['requested_sort'],
|
||||
}
|
||||
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
def _get_assets_for_page(request, course_key, options):
|
||||
"""
|
||||
Returns the list of assets for the specified page and page size.
|
||||
"""
|
||||
def _parse_request_to_dictionary(request):
|
||||
return {
|
||||
'requested_page': int(_get_requested_attribute(request, 'page')),
|
||||
'requested_page_size': int(_get_requested_attribute(request, 'page_size')),
|
||||
'requested_sort': _get_requested_attribute(request, 'sort'),
|
||||
'requested_sort_direction': _get_requested_attribute(request, 'direction'),
|
||||
'requested_asset_type': _get_requested_attribute(request, 'asset_type')
|
||||
}
|
||||
|
||||
|
||||
def _get_requested_attribute(request, attribute):
|
||||
return request.GET.get(attribute, REQUEST_DEFAULTS.get(attribute))
|
||||
|
||||
|
||||
def _get_error_if_invalid_parameters(requested_filter):
|
||||
requested_file_types = _get_requested_file_types_from_requested_filter(requested_filter)
|
||||
invalid_filters = []
|
||||
|
||||
# OTHER is not described in the settings file as a filter
|
||||
all_valid_file_types = set(_get_files_and_upload_type_filters().keys())
|
||||
all_valid_file_types.add('OTHER')
|
||||
|
||||
for requested_file_type in requested_file_types:
|
||||
if requested_file_type not in all_valid_file_types:
|
||||
invalid_filters.append(requested_file_type)
|
||||
|
||||
if invalid_filters:
|
||||
error_message = {
|
||||
'error_code': 'invalid_asset_type_filter',
|
||||
'developer_message': 'The asset_type parameter to the request is invalid. '
|
||||
'The {} filters are not described in the settings.FILES_AND_UPLOAD_TYPE_FILTERS '
|
||||
'dictionary.'.format(invalid_filters)
|
||||
}
|
||||
return JsonResponse({'error': error_message}, status=400)
|
||||
|
||||
|
||||
def _get_filter_parameters_for_mongo(requested_filter):
|
||||
requested_file_types = _get_requested_file_types_from_requested_filter(requested_filter)
|
||||
mongo_where_operator_parameters = _get_mongo_where_operator_parameters_for_filters(requested_file_types)
|
||||
|
||||
return mongo_where_operator_parameters
|
||||
|
||||
|
||||
def _get_mongo_where_operator_parameters_for_filters(requested_file_types):
|
||||
javascript_filters = []
|
||||
|
||||
for requested_file_type in requested_file_types:
|
||||
if requested_file_type == 'OTHER':
|
||||
javascript_filters_for_file_types = _get_javascript_expressions_for_other_()
|
||||
javascript_filters.append(javascript_filters_for_file_types)
|
||||
else:
|
||||
javascript_filters_for_file_types = _get_javascript_expressions_for_filter(requested_file_type)
|
||||
javascript_filters.append(javascript_filters_for_file_types)
|
||||
|
||||
javascript_filters = _join_javascript_expressions_for_filters_with_separator(javascript_filters, '||')
|
||||
|
||||
return _format_javascript_filters_for_mongo_where(javascript_filters)
|
||||
|
||||
|
||||
def _format_javascript_filters_for_mongo_where(javascript_filters):
|
||||
return {
|
||||
'$where': javascript_filters,
|
||||
}
|
||||
|
||||
|
||||
def _get_javascript_expressions_for_other_():
|
||||
file_extensions_for_requested_file_types = _get_files_and_upload_type_filters().values()
|
||||
file_extensions_for_requested_file_types_flattened = [extension for extensions in
|
||||
file_extensions_for_requested_file_types for extension in
|
||||
extensions]
|
||||
|
||||
javascript_expression_to_filter_extensions = _get_javascript_expressions_to_filter_extensions_with_operator(
|
||||
file_extensions_for_requested_file_types_flattened, '!=')
|
||||
joined_javascript_expressions_to_filter_extensions = _join_javascript_expressions_for_filters_with_separator(
|
||||
javascript_expression_to_filter_extensions, ' && ')
|
||||
|
||||
return joined_javascript_expressions_to_filter_extensions
|
||||
|
||||
|
||||
def _get_javascript_expressions_for_filter(requested_file_type):
|
||||
file_extensions_for_requested_file_type = _get_extensions_for_file_type(requested_file_type)
|
||||
javascript_expressions_to_filter_extensions = _get_javascript_expressions_to_filter_extensions_with_operator(
|
||||
file_extensions_for_requested_file_type, '==')
|
||||
joined_javascript_expressions_to_filter_extensions = _join_javascript_expressions_for_filters_with_separator(
|
||||
javascript_expressions_to_filter_extensions, ' || ')
|
||||
|
||||
return joined_javascript_expressions_to_filter_extensions
|
||||
|
||||
|
||||
def _get_files_and_upload_type_filters():
|
||||
return settings.FILES_AND_UPLOAD_TYPE_FILTERS
|
||||
|
||||
|
||||
def _get_requested_file_types_from_requested_filter(requested_filter):
|
||||
return requested_filter.split(',')
|
||||
|
||||
|
||||
def _get_extensions_for_file_type(requested_file_type):
|
||||
return _get_files_and_upload_type_filters().get(requested_file_type)
|
||||
|
||||
|
||||
def _get_javascript_expressions_to_filter_extensions_with_operator(file_extensions, operator):
|
||||
return ["JSON.stringify(this.contentType).toUpperCase() " + operator + " JSON.stringify('{}').toUpperCase()".format(
|
||||
file_extension) for file_extension in file_extensions]
|
||||
|
||||
|
||||
def _join_javascript_expressions_for_filters_with_separator(javascript_expressions_for_filtering, separator):
|
||||
return separator.join(javascript_expressions_for_filtering)
|
||||
|
||||
|
||||
def _get_sort_type_and_direction(request_options):
|
||||
sort_type = _get_mongo_sort_from_requested_sort(request_options['requested_sort'])
|
||||
sort_direction = _get_sort_direction_from_requested_sort(request_options['requested_sort_direction'])
|
||||
return [(sort_type, sort_direction)]
|
||||
|
||||
|
||||
def _get_mongo_sort_from_requested_sort(requested_sort):
|
||||
if requested_sort == 'date_added':
|
||||
sort = 'uploadDate'
|
||||
elif requested_sort == 'display_name':
|
||||
sort = 'displayname'
|
||||
else:
|
||||
sort = requested_sort
|
||||
return sort
|
||||
|
||||
|
||||
def _get_sort_direction_from_requested_sort(requested_sort_direction):
|
||||
if requested_sort_direction.lower() == 'asc':
|
||||
return ASCENDING
|
||||
|
||||
return DESCENDING
|
||||
|
||||
|
||||
def _get_current_page(requested_page):
|
||||
return max(requested_page, 0)
|
||||
|
||||
|
||||
def _get_first_asset_index(current_page, page_size):
|
||||
return current_page * page_size
|
||||
|
||||
|
||||
def _get_assets_for_page(course_key, options):
|
||||
current_page = options['current_page']
|
||||
page_size = options['page_size']
|
||||
sort = options['sort']
|
||||
@@ -197,50 +311,54 @@ def _get_assets_for_page(request, course_key, options):
|
||||
)
|
||||
|
||||
|
||||
def get_file_size(upload_file):
|
||||
"""
|
||||
Helper method for getting file size of an upload file.
|
||||
Can be used for mocking test file sizes.
|
||||
"""
|
||||
return upload_file.size
|
||||
def _update_options_to_requery_final_page(query_options, total_asset_count):
|
||||
query_options['current_page'] = int(math.floor((total_asset_count - 1) / query_options['page_size']))
|
||||
|
||||
|
||||
def _get_assets_in_json_format(assets, course_key):
|
||||
assets_in_json_format = []
|
||||
for asset in assets:
|
||||
thumbnail_asset_key = _get_thumbnail_asset_key(asset, course_key)
|
||||
asset_is_locked = asset.get('locked', False)
|
||||
|
||||
asset_in_json = _get_asset_json(
|
||||
asset['displayname'],
|
||||
asset['contentType'],
|
||||
asset['uploadDate'],
|
||||
asset['asset_key'],
|
||||
thumbnail_asset_key,
|
||||
asset_is_locked
|
||||
)
|
||||
|
||||
assets_in_json_format.append(asset_in_json)
|
||||
|
||||
return assets_in_json_format
|
||||
|
||||
|
||||
def update_course_run_asset(course_key, upload_file):
|
||||
filename = upload_file.name
|
||||
mime_type = upload_file.content_type
|
||||
size = get_file_size(upload_file)
|
||||
course_exists_response = _get_error_if_course_does_not_exist(course_key)
|
||||
|
||||
max_size_in_mb = settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB
|
||||
max_file_size_in_bytes = max_size_in_mb * 1000 ** 2
|
||||
if size > max_file_size_in_bytes:
|
||||
msg = 'File {filename} exceeds the maximum size of {max_size_in_mb} MB.'.format(
|
||||
filename=filename,
|
||||
max_size_in_mb=max_size_in_mb
|
||||
)
|
||||
raise AssetSizeTooLargeException(msg)
|
||||
if course_exists_response is not None:
|
||||
return course_exists_response
|
||||
|
||||
content_loc = StaticContent.compute_location(course_key, filename)
|
||||
file_metadata = _get_file_metadata_as_dictionary(upload_file)
|
||||
|
||||
chunked = upload_file.multiple_chunks()
|
||||
sc_partial = partial(StaticContent, content_loc, filename, mime_type)
|
||||
if chunked:
|
||||
content = sc_partial(upload_file.chunks())
|
||||
tempfile_path = upload_file.temporary_file_path()
|
||||
else:
|
||||
content = sc_partial(upload_file.read())
|
||||
tempfile_path = None
|
||||
is_file_too_large = _check_file_size_is_too_large(file_metadata)
|
||||
if is_file_too_large:
|
||||
error_message = _get_file_too_large_error_message(file_metadata['filename'])
|
||||
raise AssetSizeTooLargeException(error_message)
|
||||
|
||||
# Verify a thumbnail can be created
|
||||
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content, tempfile_path=tempfile_path)
|
||||
content, temporary_file_path = _get_file_content_and_path(file_metadata, course_key)
|
||||
|
||||
(thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail(content,
|
||||
tempfile_path=temporary_file_path)
|
||||
|
||||
# delete cached thumbnail even if one couldn't be created this time (else the old thumbnail will continue to show)
|
||||
del_cached_content(thumbnail_location)
|
||||
|
||||
# now store thumbnail location only if we could create it
|
||||
if thumbnail_content is not None:
|
||||
if _check_thumbnail_uploaded(thumbnail_content):
|
||||
content.thumbnail_location = thumbnail_location
|
||||
|
||||
# then commit the content
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
@@ -251,18 +369,10 @@ def update_course_run_asset(course_key, upload_file):
|
||||
@ensure_csrf_cookie
|
||||
@login_required
|
||||
def _upload_asset(request, course_key):
|
||||
"""
|
||||
This method allows for POST uploading of files into the course asset
|
||||
library, which will be supported by GridFS in MongoDB.
|
||||
"""
|
||||
# Does the course actually exist?!? Get anything from it to prove its
|
||||
# existence
|
||||
try:
|
||||
modulestore().get_course(course_key)
|
||||
except ItemNotFoundError:
|
||||
# no return it as a Bad Request response
|
||||
logging.error("Could not find course: %s", course_key)
|
||||
return HttpResponseBadRequest()
|
||||
course_exists_error = _get_error_if_course_does_not_exist(course_key)
|
||||
|
||||
if course_exists_error is not None:
|
||||
return course_exists_error
|
||||
|
||||
# compute a 'filename' which is similar to the location formatting, we're
|
||||
# using the 'filename' nomenclature since we're using a FileSystem paradigm
|
||||
@@ -272,8 +382,8 @@ def _upload_asset(request, course_key):
|
||||
|
||||
try:
|
||||
content = update_course_run_asset(course_key, upload_file)
|
||||
except AssetSizeTooLargeException as ex:
|
||||
return JsonResponse({'error': ex.message}, status=413)
|
||||
except AssetSizeTooLargeException as exception:
|
||||
return JsonResponse({'error': exception.message}, status=413)
|
||||
|
||||
# readback the saved content - we need the database timestamp
|
||||
readback = contentstore().find(content.location)
|
||||
@@ -291,16 +401,93 @@ def _upload_asset(request, course_key):
|
||||
})
|
||||
|
||||
|
||||
@require_http_methods(("DELETE", "POST", "PUT"))
|
||||
def _get_error_if_course_does_not_exist(course_key):
|
||||
try:
|
||||
modulestore().get_course(course_key)
|
||||
except ItemNotFoundError:
|
||||
logging.error('Could not find course: %s', course_key)
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
def _get_file_metadata_as_dictionary(upload_file):
|
||||
# compute a 'filename' which is similar to the location formatting; we're
|
||||
# using the 'filename' nomenclature since we're using a FileSystem paradigm
|
||||
# here; we're just imposing the Location string formatting expectations to
|
||||
# keep things a bit more consistent
|
||||
return {
|
||||
'upload_file': upload_file,
|
||||
'filename': upload_file.name,
|
||||
'mime_type': upload_file.content_type,
|
||||
'upload_file_size': get_file_size(upload_file)
|
||||
}
|
||||
|
||||
|
||||
def get_file_size(upload_file):
|
||||
# can be used for mocking test file sizes.
|
||||
return upload_file.size
|
||||
|
||||
|
||||
def _check_file_size_is_too_large(file_metadata):
|
||||
upload_file_size = file_metadata['upload_file_size']
|
||||
maximum_file_size_in_megabytes = settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB
|
||||
maximum_file_size_in_bytes = maximum_file_size_in_megabytes * 1000 ** 2
|
||||
|
||||
return upload_file_size > maximum_file_size_in_bytes
|
||||
|
||||
|
||||
def _get_file_too_large_error_message(filename):
|
||||
return _(
|
||||
'File {filename} exceeds maximum size of '
|
||||
'{maximum_size_in_megabytes} MB.'
|
||||
).format(
|
||||
filename=filename,
|
||||
maximum_size_in_megabytes=settings.MAX_ASSET_UPLOAD_FILE_SIZE_IN_MB,
|
||||
)
|
||||
|
||||
|
||||
def _get_file_content_and_path(file_metadata, course_key):
|
||||
content_location = StaticContent.compute_location(course_key, file_metadata['filename'])
|
||||
upload_file = file_metadata['upload_file']
|
||||
|
||||
file_can_be_chunked = upload_file.multiple_chunks()
|
||||
|
||||
static_content_partial = partial(StaticContent, content_location, file_metadata['filename'],
|
||||
file_metadata['mime_type'])
|
||||
|
||||
if file_can_be_chunked:
|
||||
content = static_content_partial(upload_file.chunks())
|
||||
temporary_file_path = upload_file.temporary_file_path()
|
||||
else:
|
||||
content = static_content_partial(upload_file.read())
|
||||
temporary_file_path = None
|
||||
return content, temporary_file_path
|
||||
|
||||
|
||||
def _check_thumbnail_uploaded(thumbnail_content):
|
||||
return thumbnail_content is not None
|
||||
|
||||
|
||||
def _get_thumbnail_asset_key(asset, course_key):
|
||||
# note, due to the schema change we may not have a 'thumbnail_location' in the result set
|
||||
thumbnail_location = asset.get('thumbnail_location', None)
|
||||
thumbnail_asset_key = None
|
||||
|
||||
if thumbnail_location:
|
||||
thumbnail_path = thumbnail_location[4]
|
||||
thumbnail_asset_key = course_key.make_asset_key('thumbnail', thumbnail_path)
|
||||
return thumbnail_asset_key
|
||||
|
||||
|
||||
@require_http_methods(('DELETE', 'POST', 'PUT'))
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
def _update_asset(request, course_key, asset_key):
|
||||
"""
|
||||
'''
|
||||
restful CRUD operations for a course asset.
|
||||
Currently only DELETE, POST, and PUT methods are implemented.
|
||||
|
||||
asset_path_encoding: the odd /c4x/org/course/category/name repr of the asset (used by Backbone as the id)
|
||||
"""
|
||||
'''
|
||||
if request.method == 'DELETE':
|
||||
try:
|
||||
delete_asset(course_key, asset_key)
|
||||
@@ -311,56 +498,60 @@ def _update_asset(request, course_key, asset_key):
|
||||
elif request.method in ('PUT', 'POST'):
|
||||
if 'file' in request.FILES:
|
||||
return _upload_asset(request, course_key)
|
||||
else:
|
||||
# Update existing asset
|
||||
try:
|
||||
modified_asset = json.loads(request.body)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
contentstore().set_attr(asset_key, 'locked', modified_asset['locked'])
|
||||
# Delete the asset from the cache so we check the lock status the next time it is requested.
|
||||
del_cached_content(asset_key)
|
||||
return JsonResponse(modified_asset, status=201)
|
||||
|
||||
# update existing asset
|
||||
try:
|
||||
modified_asset = json.loads(request.body)
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest()
|
||||
contentstore().set_attr(asset_key, 'locked', modified_asset['locked'])
|
||||
# delete the asset from the cache so we check the lock status the next time it is requested.
|
||||
del_cached_content(asset_key)
|
||||
return JsonResponse(modified_asset, status=201)
|
||||
|
||||
|
||||
def _save_content_to_trash(content):
|
||||
contentstore('trashcan').save(content)
|
||||
|
||||
|
||||
def delete_asset(course_key, asset_key):
|
||||
"""
|
||||
Deletes asset represented by given 'asset_key' in the course represented by given course_key.
|
||||
"""
|
||||
# Make sure the item to delete actually exists.
|
||||
content = _check_existence_and_get_asset_content(asset_key)
|
||||
|
||||
_save_content_to_trash(content)
|
||||
|
||||
_delete_thumbnail(content.thumbnail_location, course_key, asset_key)
|
||||
contentstore().delete(content.get_id())
|
||||
del_cached_content(content.location)
|
||||
|
||||
|
||||
def _check_existence_and_get_asset_content(asset_key):
|
||||
try:
|
||||
content = contentstore().find(asset_key)
|
||||
return content
|
||||
except NotFoundError:
|
||||
raise AssetNotFoundException
|
||||
|
||||
# ok, save the content into the trashcan
|
||||
contentstore('trashcan').save(content)
|
||||
|
||||
# see if there is a thumbnail as well, if so move that as well
|
||||
if content.thumbnail_location is not None:
|
||||
def _delete_thumbnail(thumbnail_location, course_key, asset_key):
|
||||
if thumbnail_location is not None:
|
||||
|
||||
# We are ignoring the value of the thumbnail_location-- we only care whether
|
||||
# or not a thumbnail has been stored, and we can now easily create the correct path.
|
||||
thumbnail_location = course_key.make_asset_key('thumbnail', asset_key.name)
|
||||
|
||||
try:
|
||||
thumbnail_content = contentstore().find(thumbnail_location)
|
||||
contentstore('trashcan').save(thumbnail_content)
|
||||
# hard delete thumbnail from origin
|
||||
_save_content_to_trash(thumbnail_content)
|
||||
contentstore().delete(thumbnail_content.get_id())
|
||||
# remove from any caching
|
||||
del_cached_content(thumbnail_location)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logging.warning('Could not delete thumbnail: %s', thumbnail_location)
|
||||
|
||||
# delete the original
|
||||
contentstore().delete(content.get_id())
|
||||
# remove from cache
|
||||
del_cached_content(content.location)
|
||||
|
||||
|
||||
def _get_asset_json(display_name, content_type, date, location, thumbnail_location, locked):
|
||||
"""
|
||||
'''
|
||||
Helper method for formatting the asset information to send to client.
|
||||
"""
|
||||
'''
|
||||
asset_url = StaticContent.serialize_asset_key_with_slash(location)
|
||||
external_url = settings.LMS_BASE + asset_url
|
||||
return {
|
||||
@@ -372,6 +563,6 @@ def _get_asset_json(display_name, content_type, date, location, thumbnail_locati
|
||||
'portable_url': StaticContent.get_static_path_from_location(location),
|
||||
'thumbnail': StaticContent.serialize_asset_key_with_slash(thumbnail_location) if thumbnail_location else None,
|
||||
'locked': locked,
|
||||
# Needed for Backbone delete/update.
|
||||
# needed for Backbone delete/update.
|
||||
'id': unicode(location)
|
||||
}
|
||||
|
||||
@@ -174,6 +174,15 @@ class PaginationTestCase(AssetsTestCase):
|
||||
self.assert_correct_filter_response(self.url, 'asset_type', 'OTHER')
|
||||
self.assert_correct_filter_response(
|
||||
self.url, 'asset_type', 'Documents')
|
||||
self.assert_correct_filter_response(
|
||||
self.url, 'asset_type', 'Documents,Images')
|
||||
self.assert_correct_filter_response(
|
||||
self.url, 'asset_type', 'Documents,OTHER')
|
||||
|
||||
#Verify invalid request parameters
|
||||
self.assert_invalid_parameters_error(self.url, 'asset_type', 'edX')
|
||||
self.assert_invalid_parameters_error(self.url, 'asset_type', 'edX, OTHER')
|
||||
self.assert_invalid_parameters_error(self.url, 'asset_type', 'edX, Images')
|
||||
|
||||
# Verify querying outside the range of valid pages
|
||||
self.assert_correct_asset_response(
|
||||
@@ -247,24 +256,46 @@ class PaginationTestCase(AssetsTestCase):
|
||||
"""
|
||||
Get from the url w/ a filter option and ensure items honor that filter
|
||||
"""
|
||||
requested_file_types = settings.FILES_AND_UPLOAD_TYPE_FILTERS.get(
|
||||
filter_value, None)
|
||||
|
||||
filter_value_split = filter_value.split(',')
|
||||
|
||||
requested_file_extensions = []
|
||||
all_file_extensions = []
|
||||
|
||||
for requested_filter in filter_value_split:
|
||||
if requested_filter == 'OTHER':
|
||||
for file_type in settings.FILES_AND_UPLOAD_TYPE_FILTERS:
|
||||
all_file_extensions.extend(file_type)
|
||||
else:
|
||||
file_extensions = settings.FILES_AND_UPLOAD_TYPE_FILTERS.get(
|
||||
requested_filter, None)
|
||||
if file_extensions is not None:
|
||||
requested_file_extensions.extend(file_extensions)
|
||||
|
||||
resp = self.client.get(
|
||||
url + '?' + filter_type + '=' + filter_value, HTTP_ACCEPT='application/json')
|
||||
json_response = json.loads(resp.content)
|
||||
assets_response = json_response['assets']
|
||||
|
||||
if filter_value is not '':
|
||||
content_types = [asset['content_type'].lower()
|
||||
for asset in assets_response]
|
||||
if filter_value is 'OTHER':
|
||||
all_file_type_extensions = []
|
||||
for file_type in settings.FILES_AND_UPLOAD_TYPE_FILTERS:
|
||||
all_file_type_extensions.extend(file_type)
|
||||
if 'OTHER' in filter_value_split:
|
||||
for content_type in content_types:
|
||||
self.assertNotIn(content_type, all_file_type_extensions)
|
||||
# content_type is either not any defined type (i.e. OTHER) or is a defined type (if multiple
|
||||
# parameters including OTHER are used)
|
||||
self.assertTrue(content_type in requested_file_extensions or content_type not in all_file_extensions)
|
||||
else:
|
||||
for content_type in content_types:
|
||||
self.assertIn(content_type, requested_file_types)
|
||||
self.assertIn(content_type, requested_file_extensions)
|
||||
|
||||
def assert_invalid_parameters_error(self, url, filter_type, filter_value):
|
||||
"""
|
||||
Get from the url w/ invalid filter option(s) and ensure error is received
|
||||
"""
|
||||
resp = self.client.get(
|
||||
url + '?' + filter_type + '=' + filter_value, HTTP_ACCEPT='application/json')
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
|
||||
@ddt
|
||||
|
||||
@@ -1257,7 +1257,8 @@ ADVANCED_PROBLEM_TYPES = [
|
||||
# Files and Uploads type filter values
|
||||
|
||||
FILES_AND_UPLOAD_TYPE_FILTERS = {
|
||||
"Images": ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/tiff', 'image/tif', 'image/x-icon'],
|
||||
"Images": ['image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/tiff', 'image/tif', 'image/x-icon',
|
||||
'image/svg+xml', 'image/bmp', 'image/x-ms-bmp', ],
|
||||
"Documents": [
|
||||
'application/pdf',
|
||||
'text/plain',
|
||||
@@ -1271,7 +1272,25 @@ FILES_AND_UPLOAD_TYPE_FILTERS = {
|
||||
'application/msword',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/csv',
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||
'text/x-tex',
|
||||
'application/x-pdf',
|
||||
'application/vnd.ms-excel.sheet.macroenabled.12',
|
||||
'file/pdf',
|
||||
'image/pdf',
|
||||
'text/csv',
|
||||
'text/pdf',
|
||||
'text/x-sh',
|
||||
'\application/pdf\""',
|
||||
],
|
||||
"Audio": ['audio/mpeg', 'audio/mp3', 'audio/x-wav', 'audio/ogg', 'audio/wav', 'audio/aac', 'audio/x-m4a',
|
||||
'audio/mp4', 'audio/x-ms-wma', ],
|
||||
"Code": ['application/json', 'text/html', 'text/javascript', 'application/javascript', 'text/css', 'text/x-python',
|
||||
'application/x-java-jnlp-file', 'application/xml', 'application/postscript', 'application/x-javascript',
|
||||
'application/java-vm', 'text/x-c++src', 'text/xml', 'text/x-scss', 'application/x-python-code',
|
||||
'application/java-archive', 'text/x-python-script', 'application/x-ruby', 'application/mathematica',
|
||||
'text/coffeescript', 'text/x-matlab', 'application/sql', 'text/php', ]
|
||||
}
|
||||
|
||||
# Default to no Search Engine
|
||||
|
||||
Reference in New Issue
Block a user