diff --git a/apps/documents/models.py b/apps/documents/models.py index fd834619a4..ff95d887eb 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -323,5 +323,16 @@ class RecentDocument(models.Model): # Register the fields that will be searchable -register(Document, _(u'document'), [u'document_type__name', u'file_mimetype', u'file_filename', u'file_extension', u'documentmetadata__value', u'documentpage__content', u'description', u'tags__name', u'comments__comment']) +register('document', Document, _(u'document'), [ + {'name': u'document_type__name', 'title': _(u'Document type')}, + {'name': u'file_mimetype', 'title': _(u'MIME type')}, + {'name': u'file_filename', 'title': _(u'Filename')}, + {'name': u'file_extension', 'title': _(u'Filename extension')}, + {'name': u'documentmetadata__value', 'title': _(u'Metadata value')}, + {'name': u'documentpage__content', 'title': _(u'Content')}, + {'name': u'description', 'title': _(u'Description')}, + {'name': u'tags__name', 'title': _(u'Tags')}, + {'name': u'comments__comment', 'title': _(u'Comments')}, + ] +) #register(Document, _(u'document'), ['document_type__name', 'file_mimetype', 'file_extension', 'documentmetadata__value', 'documentpage__content', 'description', {'field_name':'file_filename', 'comparison':'iexact'}]) diff --git a/apps/dynamic_search/__init__.py b/apps/dynamic_search/__init__.py index b51382045f..0e76595f36 100644 --- a/apps/dynamic_search/__init__.py +++ b/apps/dynamic_search/__init__.py @@ -1,3 +1,10 @@ -from navigation.api import register_sidebar_template +from django.utils.translation import ugettext_lazy as _ -register_sidebar_template(['search'], 'search_help.html') +from navigation.api import register_sidebar_template, register_links + +search = {'text': _(u'search'), 'view': 'search', 'famfam': 'zoom'} +search_advanced = {'text': _(u'advanced search'), 'view': 'search_advanced', 'famfam': 'zoom_in'} + +register_sidebar_template(['search', 'search_advanced'], 'search_help.html') + +register_links(['search', 'search_advanced'], [search, search_advanced], menu_name='form_header') diff --git a/apps/dynamic_search/api.py b/apps/dynamic_search/api.py index 01cb544e55..014eee62d9 100644 --- a/apps/dynamic_search/api.py +++ b/apps/dynamic_search/api.py @@ -9,34 +9,31 @@ from django.db.models import Q from dynamic_search.conf.settings import LIMIT -search_list = {} +registered_search_dict = {} -def register(model, text, field_list): - if model in search_list: - search_list[model]['fields'].append(field_list) - else: - search_list[model] = {'fields': field_list, 'text': text} +def register(model_name, model, title, fields): + registered_search_dict.setdefault(model_name, {'model': model, 'fields': [], 'title': title}) + registered_search_dict[model_name]['fields'].extend(fields) def normalize_query(query_string, findterms=re.compile(r'"([^"]+)"|(\S+)').findall, normspace=re.compile(r'\s{2,}').sub): - """ Splits the query string in invidual keywords, getting rid of unecessary spaces - and grouping quoted words together. - Example: - + """ + Splits the query string in invidual keywords, getting rid of unecessary spaces + and grouping quoted words together. + Example: >>> normalize_query(' some random words "with quotes " and spaces') ['some', 'random', 'words', 'with quotes', 'and', 'spaces'] - """ return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] def get_query(terms, search_fields): """ - Returns a query, that is a combination of Q objects. That combination - aims to search keywords within a model by testing the given search fields. + Returns a query, that is a combination of Q objects. That combination + aims to search keywords within a model by testing the given search fields. """ queries = [] for term in terms: @@ -58,21 +55,55 @@ def get_query(terms, search_fields): queries.append(or_query) return queries + - -def perform_search(query_string): +def perform_search(query_string, field_list=None): model_list = {} flat_list = [] result_count = 0 shown_result_count = 0 elapsed_time = 0 + start_time = datetime.datetime.now() + + search_dict = {} if query_string: - start_time = datetime.datetime.now() - terms = normalize_query(query_string) + simple_query_string = query_string.get('q', u'').strip() + if simple_query_string: + for model, values in registered_search_dict.items(): + search_dict.setdefault(values['model'], {'query_entries': [], 'title': values['title']}) + field_names = [field['name'] for field in values['fields']] + # One entry, single set of terms for all fields names + search_dict[values['model']]['query_entries'].append( + { + 'field_name': field_names, + 'terms': normalize_query(simple_query_string) + } + ) + else: + for key, value in query_string.items(): + try: + model, field_name = key.split('__', 1) + model_entry = registered_search_dict.get(model, {}) + if model_entry: + for model_field in model_entry.get('fields', [{}]): + if model_field.get('name') == field_name: + search_dict.setdefault(model_entry['model'], {'query_entries': [], 'title': model_entry['title']}) + search_dict[model_entry['model']]['query_entries'].append( + { + 'field_name': [field_name], + 'terms': normalize_query(value.strip()) + } + ) + except ValueError: + pass - for model, data in search_list.items(): - queries = get_query(terms, data['fields']) + for model, data in search_dict.items(): + title = data['title'] + queries = [] + + for query_entry in data['query_entries']: + queries.extend(get_query(query_entry['terms'], query_entry['field_name'])) model_result_ids = None for query in queries: @@ -84,14 +115,24 @@ def perform_search(query_string): else: model_result_ids &= single_result_ids + if model_result_ids == None: + model_result_ids = [] + result_count += len(model_result_ids) results = model.objects.in_bulk(list(model_result_ids)[: LIMIT]).values() shown_result_count += len(results) if results: - model_list[data['text']] = results + model_list[title] = results for result in results: if result not in flat_list: flat_list.append(result) + elapsed_time = unicode(datetime.datetime.now() - start_time).split(':')[2] - return model_list, flat_list, shown_result_count, result_count, elapsed_time + return { + 'model_list': model_list, + 'flat_list': flat_list, + 'shown_result_count': shown_result_count, + 'result_count': result_count, + 'elapsed_time': elapsed_time + } diff --git a/apps/dynamic_search/forms.py b/apps/dynamic_search/forms.py index 57925d2aef..ad10b084a8 100644 --- a/apps/dynamic_search/forms.py +++ b/apps/dynamic_search/forms.py @@ -3,4 +3,17 @@ from django.utils.translation import ugettext_lazy as _ class SearchForm(forms.Form): - q = forms.CharField(max_length=128, label=_(u'Search term')) + q = forms.CharField(max_length=128, label=_(u'Search terms')) + + +class AdvancedSearchForm(forms.Form): + def __init__(self, *args, **kwargs): + search_fields = kwargs.pop('search_fields') + super(AdvancedSearchForm, self).__init__(*args, **kwargs) + + #Set form fields initial values + for search_field in search_fields: + self.fields[search_field['name']] = forms.CharField( + label=search_field['title'], + required=False + ) diff --git a/apps/dynamic_search/urls.py b/apps/dynamic_search/urls.py index 8c7843c247..5a4ef1a80e 100644 --- a/apps/dynamic_search/urls.py +++ b/apps/dynamic_search/urls.py @@ -2,5 +2,6 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('dynamic_search.views', url(r'^search/$', 'search', (), 'search'), + url(r'^search/advanced/$', 'search', { 'advanced': True }, 'search_advanced'), url(r'^results/$', 'results', (), 'results'), ) diff --git a/apps/dynamic_search/views.py b/apps/dynamic_search/views.py index b31e5409fc..c626218a30 100644 --- a/apps/dynamic_search/views.py +++ b/apps/dynamic_search/views.py @@ -4,48 +4,18 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib import messages from django.conf import settings -from dynamic_search.api import perform_search -from dynamic_search.forms import SearchForm +from dynamic_search.api import perform_search, registered_search_dict +from dynamic_search.forms import SearchForm, AdvancedSearchForm from dynamic_search.conf.settings import SHOW_OBJECT_TYPE from dynamic_search.conf.settings import LIMIT -def results(request, form=None): +def results(request, extra_context=None): query_string = '' context = {} - if ('q' in request.GET) and request.GET['q'].strip(): - query_string = request.GET['q'] - try: - model_list, flat_list, shown_result_count, total_result_count, elapsed_time = perform_search(query_string) - if shown_result_count != total_result_count: - title = _(u'results with: %(query_string)s (showing only %(shown_result_count)s out of %(total_result_count)s)') % { - 'query_string': query_string, 'shown_result_count': shown_result_count, - 'total_result_count': total_result_count} - else: - title = _(u'results with: %s') % query_string - context.update({ - 'found_entries': model_list, - 'object_list': flat_list, - 'title': title, - 'time_delta': elapsed_time, - }) - - except Exception, e: - if settings.DEBUG: - raise - elif request.user.is_staff or request.user.is_superuser: - messages.error(request, _(u'Search error: %s') % e) - else: - context.update({ - 'found_entries': [], - 'object_list': [], - 'title': _(u'results'), - }) - context.update({ - 'query_string': query_string, - 'form': form, + 'query_string': request.GET, 'form_title': _(u'Search'), #'hide_header': True, 'form_hide_required_text': True, @@ -55,6 +25,30 @@ def results(request, form=None): 'submit_icon_famfam': 'zoom', 'search_results_limit': LIMIT, }) + + if extra_context: + context.update(extra_context) + + try: + response = perform_search(request.GET) + if response['shown_result_count'] != response['result_count']: + title = _(u'results, (showing only %(shown_result_count)s out of %(result_count)s)') % { + 'shown_result_count': response['shown_result_count'], + 'result_count': response['result_count']} + else: + title = _(u'results') + context.update({ + 'found_entries': response['model_list'], + 'object_list': response['flat_list'], + 'title': title, + 'time_delta': response['elapsed_time'], + }) + + except Exception, e: + if settings.DEBUG: + raise + elif request.user.is_staff or request.user.is_superuser: + messages.error(request, _(u'Search error: %s') % e) if SHOW_OBJECT_TYPE: context.update({'extra_columns': @@ -64,11 +58,38 @@ def results(request, form=None): context_instance=RequestContext(request)) -def search(request): - if ('q' in request.GET) and request.GET['q'].strip(): - query_string = request.GET['q'] - form = SearchForm(initial={'q': query_string}) - return results(request, form=form) +def search(request, advanced=False): + if advanced: + search_fields = [] + for model_name, values in registered_search_dict.items(): + for field in values['fields']: + search_fields.append( + { + 'title': field['title'], + 'name': '%s__%s' % (model_name, field['name']) + } + ) + form = AdvancedSearchForm( + search_fields=search_fields, + data=request.GET + ) + + return results(request, extra_context={ + 'form': form, + 'form_title': _(u'advanced search') + } + ) else: - form = SearchForm() - return results(request, form=form) + if ('q' in request.GET) and request.GET['q'].strip(): + query_string = request.GET['q'] + form = SearchForm(initial={'q': query_string}) + return results(request, extra_context={ + 'form': form + } + ) + else: + form = SearchForm() + return results(request, extra_context={ + 'form': form + } + )