Merge pull request #17119 from edx/thallada/files-search

Add filename search to Files & Uploads
This commit is contained in:
Michael Roytman
2018-01-22 21:12:47 -05:00
committed by GitHub
3 changed files with 98 additions and 56 deletions

View File

@@ -2,6 +2,7 @@ import json
import logging
import math
from functools import partial
import re
from django.conf import settings
from django.contrib.auth.decorators import login_required
@@ -34,7 +35,8 @@ REQUEST_DEFAULTS = {
'page_size': 50,
'sort': 'date_added',
'direction': '',
'asset_type': ''
'asset_type': '',
'text_search': '',
}
@@ -54,6 +56,8 @@ def assets_handler(request, course_key_string=None, asset_key_string=None):
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')
asset_type: the file type to filter items to (defaults to All)
text_search: string to filter results by file name (defaults to '')
POST
json: create (or update?) an asset. The only updating that can be done is changing the lock state.
PUT
@@ -113,7 +117,7 @@ def _assets_json(request, course_key):
'''
request_options = _parse_request_to_dictionary(request)
filter_parameters = None
filter_parameters = {}
if request_options['requested_asset_type']:
filters_are_invalid_error = _get_error_if_invalid_parameters(request_options['requested_asset_type'])
@@ -121,7 +125,10 @@ def _assets_json(request, course_key):
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'])
filter_parameters.update(_get_content_type_filter_for_mongo(request_options['requested_asset_type']))
if request_options['requested_text_search']:
filter_parameters.update(_get_displayname_search_filter_for_mongo(request_options['requested_text_search']))
sort_type_and_direction = _get_sort_type_and_direction(request_options)
@@ -157,6 +164,7 @@ def _assets_json(request, course_key):
'sort': request_options['requested_sort'],
'direction': request_options['requested_sort_direction'],
'assetTypes': _get_requested_file_types_from_requested_filter(request_options['requested_asset_type']),
'textSearch': request_options['requested_text_search'],
}
return JsonResponse(response_payload)
@@ -169,6 +177,7 @@ def _parse_request_to_dictionary(request):
'requested_sort': _get_requested_attribute(request, 'sort'),
'requested_sort_direction': _get_requested_attribute(request, 'direction'),
'requested_asset_type': _get_requested_attribute(request, 'asset_type'),
'requested_text_search': _get_requested_attribute(request, 'text_search'),
}
@@ -198,57 +207,77 @@ def _get_error_if_invalid_parameters(requested_filter):
return JsonResponse({'error': error_message}, status=400)
def _get_filter_parameters_for_mongo(requested_filter):
def _get_content_type_filter_for_mongo(requested_filter):
"""
Construct and return pymongo query dict for the given content type categories.
"""
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)
type_filter = {
"$or": []
}
return mongo_where_operator_parameters
if 'OTHER' in requested_file_types:
type_filter["$or"].append(_get_mongo_expression_for_type_other())
requested_file_types.remove('OTHER')
type_filter["$or"].append(_get_mongo_expression_for_type_filter(requested_file_types))
return type_filter
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):
def _get_mongo_expression_for_type_other():
"""
Construct and return pymongo expression dict for the 'OTHER' content type category.
"""
content_types = [ext for extensions in _get_files_and_upload_type_filters().values() for ext in extensions]
return {
'$where': javascript_filters,
'contentType': {
'$nin': content_types
}
}
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]
def _get_mongo_expression_for_type_filter(requested_file_types):
"""
Construct and return pymongo expression dict for the named content type categories.
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, ' && ')
The named content categories are the keys of the FILES_AND_UPLOAD_TYPE_FILTERS setting that are not 'OTHER':
'Images', 'Documents', 'Audio', and 'Code'.
"""
content_types = []
files_and_upload_type_filters = _get_files_and_upload_type_filters()
return joined_javascript_expressions_to_filter_extensions
for requested_file_type in requested_file_types:
content_types.extend(files_and_upload_type_filters[requested_file_type])
return {
'contentType': {
'$in': content_types
}
}
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, ' || ')
def _get_displayname_search_filter_for_mongo(text_search):
"""
Return a pymongo query dict for the given search string, using case insensitivity.
"""
filters = []
return joined_javascript_expressions_to_filter_extensions
text_search_tokens = text_search.split()
for token in text_search_tokens:
escaped_token = re.escape(token)
filters.append({
'displayname': {
'$regex': escaped_token,
'$options': 'i',
},
})
return {
'$and': filters,
}
def _get_files_and_upload_type_filters():
@@ -259,19 +288,6 @@ def _get_requested_file_types_from_requested_filter(requested_filter):
return requested_filter.split(',') if requested_filter else []
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'])
@@ -309,7 +325,6 @@ def _get_assets_for_page(course_key, options):
sort = options['sort']
filter_params = options['filter_params'] if options['filter_params'] else None
start = current_page * page_size
return contentstore().get_all_content_for_course(
course_key, start=start, maxresults=page_size, sort=sort, filter_params=filter_params
)

View File

@@ -179,6 +179,16 @@ class PaginationTestCase(AssetsTestCase):
self.assert_correct_filter_response(
self.url, 'asset_type', 'Documents,OTHER')
self.assert_correct_text_search_response(self.url, 'asset-1.txt', 1)
self.assert_correct_text_search_response(self.url, 'asset-1', 1)
self.assert_correct_text_search_response(self.url, 'AsSeT-1', 1)
self.assert_correct_text_search_response(self.url, '.txt', 3)
self.assert_correct_text_search_response(self.url, '2', 1)
self.assert_correct_text_search_response(self.url, 'asset 2', 1)
self.assert_correct_text_search_response(self.url, '2 asset', 1)
self.assert_correct_text_search_response(self.url, '*.txt', 0)
self.assert_correct_asset_response(self.url + "?text_search=", 0, 4, 4)
#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')
@@ -300,6 +310,23 @@ class PaginationTestCase(AssetsTestCase):
url + '?' + filter_type + '=' + filter_value, HTTP_ACCEPT='application/json')
self.assertEquals(resp.status_code, 400)
def assert_correct_text_search_response(self, url, text_search, number_matches):
"""
Get from the url w/ a text_search option and ensure items honor that search query
"""
resp = self.client.get(
url + '?text_search=' + text_search, HTTP_ACCEPT='application/json')
json_response = json.loads(resp.content)
assets_response = json_response['assets']
self.assertEquals(text_search, json_response['textSearch'])
self.assertEquals(len(assets_response), number_matches)
text_search_tokens = text_search.split()
for asset_response in assets_response:
for token in text_search_tokens:
self.assertTrue(token.lower() in asset_response['display_name'].lower())
@ddt
class UploadTestCase(AssetsTestCase):

View File

@@ -1331,7 +1331,7 @@ FILES_AND_UPLOAD_TYPE_FILTERS = {
'text/csv',
'text/pdf',
'text/x-sh',
'\application/pdf\""',
'\"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', ],