diff --git a/cms/djangoapps/contentstore/views/assets.py b/cms/djangoapps/contentstore/views/assets.py index 27752c070c..cd782a598c 100644 --- a/cms/djangoapps/contentstore/views/assets.py +++ b/cms/djangoapps/contentstore/views/assets.py @@ -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) } diff --git a/cms/djangoapps/contentstore/views/tests/test_assets.py b/cms/djangoapps/contentstore/views/tests/test_assets.py index 4275c33cf6..cde9a45829 100644 --- a/cms/djangoapps/contentstore/views/tests/test_assets.py +++ b/cms/djangoapps/contentstore/views/tests/test_assets.py @@ -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 diff --git a/cms/envs/common.py b/cms/envs/common.py index 3d38695818..d697a131c7 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -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