From fb50f79634c6d4cc54b13b69bbffc8ea4758d5d0 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Thu, 16 Jul 2020 09:37:21 -0400 Subject: [PATCH 1/3] Add support for search_fields and split_words Close #783 --- src/dal/views.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/dal/views.py b/src/dal/views.py index fc2db24ee..34630f600 100644 --- a/src/dal/views.py +++ b/src/dal/views.py @@ -1,11 +1,15 @@ """Base views for autocomplete widgets.""" import json +import operator +from functools import reduce import django from django import http +from django.contrib.admin.utils import lookup_needs_distinct from django.contrib.auth import get_permission_codename from django.core.exceptions import ImproperlyConfigured +from django.db.models import Q from django.http import HttpResponseBadRequest, HttpResponseNotAllowed from django.views.generic.list import BaseListView @@ -71,6 +75,8 @@ class BaseQuerySetView(ViewMixin, BaseListView): context_object_name = 'results' model_field_name = 'name' create_field = None + search_fields = [] + split_words = None def has_more(self, context): """For widgets that have infinite-scroll feature.""" @@ -92,11 +98,55 @@ def get_queryset(self): """Filter the queryset with GET['q'].""" qs = super(BaseQuerySetView, self).get_queryset() - if self.q: - qs = qs.filter(**{'%s__icontains' % self.model_field_name: self.q}) + qs = self.get_search_results(qs, self.q) return qs + def get_search_fields(self): + """Get the fields to search over.""" + if self.search_fields: + return self.search_fields + else: + return [self.model_field_name] + + def _construct_search(self, field_name): + """Apply keyword searches.""" + if field_name.startswith("^"): + return "%s__istartswith" % field_name[1:] + elif field_name.startswith("="): + return "%s__iexact" % field_name[1:] + elif field_name.startswith("@"): + return "%s__search" % field_name[1:] + else: + return "%s__icontains" % field_name + + def get_search_results(self, queryset, search_term): + """Filter the results based on the query.""" + search_fields = self.get_search_fields() + if search_fields and search_term: + orm_lookups = [ + self._construct_search(search_field) for search_field in search_fields + ] + if self.split_words is not None: + word_conditions = [] + for word in search_term.split(): + or_queries = [Q(**{orm_lookup: word}) for orm_lookup in orm_lookups] + word_conditions.append(reduce(operator.or_, or_queries)) + op_ = operator.or_ if self.split_words == "or" else operator.and_ + queryset = queryset.filter(reduce(op_, word_conditions)) + else: + or_queries = [ + Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups + ] + queryset = queryset.filter(reduce(operator.or_, or_queries)) + + for search_spec in orm_lookups: + if lookup_needs_distinct(queryset.model._meta, search_spec): + queryset = queryset.distinct() + break + + return queryset + def create_object(self, text): """Create an object given a text.""" return self.get_queryset().get_or_create( From a747f0e9ab8725e02a975a4d20e670c70bb35b73 Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Thu, 16 Jul 2020 09:39:42 -0400 Subject: [PATCH 2/3] Add support for template property --- src/dal/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/dal/views.py b/src/dal/views.py index 34630f600..5ba938e12 100644 --- a/src/dal/views.py +++ b/src/dal/views.py @@ -11,6 +11,7 @@ from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from django.http import HttpResponseBadRequest, HttpResponseNotAllowed +from django.template.loader import render_to_string from django.views.generic.list import BaseListView import six @@ -77,6 +78,7 @@ class BaseQuerySetView(ViewMixin, BaseListView): create_field = None search_fields = [] split_words = None + template = None def has_more(self, context): """For widgets that have infinite-scroll feature.""" @@ -88,7 +90,10 @@ def get_result_value(self, result): def get_result_label(self, result): """Return the label of a result.""" - return six.text_type(result) + if self.template: + return render_to_string(self.template, {"result": result}) + else: + return six.text_type(result) def get_selected_result_label(self, result): """Return the label of a selected result.""" From d1984d45ddc3fa1b295d3363e9169f0ed50c872a Mon Sep 17 00:00:00 2001 From: Mitchell Kotler Date: Thu, 23 Jul 2020 09:40:27 -0400 Subject: [PATCH 3/3] More efficient distinct checking Taken from Django code --- src/dal/views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dal/views.py b/src/dal/views.py index 5ba938e12..727d24b4e 100644 --- a/src/dal/views.py +++ b/src/dal/views.py @@ -145,10 +145,11 @@ def get_search_results(self, queryset, search_term): ] queryset = queryset.filter(reduce(operator.or_, or_queries)) - for search_spec in orm_lookups: - if lookup_needs_distinct(queryset.model._meta, search_spec): - queryset = queryset.distinct() - break + if any( + lookup_needs_distinct(queryset.model._meta, search_spec) + for search_spec in orm_lookups + ): + queryset = queryset.distinct() return queryset