# Imports
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.utils.translation import gettext_lazy as _
from django.views import View
import logging
import os
from superdjango.interfaces.hooks import hooks
from superdjango.ui.runtime.data import Record
from superdjango.utils import smart_cast
from superdjango.views import JSONMixin
from ..constants import VERB
from .access import UIAccessMixin
log = logging.getLogger(__name__)
# Exports
__all__ = (
"UIAjaxAutoCompleteView",
"UIAjaxChainedLookupView",
"UIAjaxChooserView",
"UIAjaxCreateView",
"UIAjaxDetailView",
"UIAjaxDragAndDropView",
"UIAjaxFormMixin",
"UIAjaxListView",
"UIAjaxMarkCompleteView",
"UIAjaxMixin",
"UIAjaxReorderView",
"UIAjaxSearchView",
"UIAjaxUpdateView",
)
# Mixins
[docs]class UIAjaxMixin(UIAccessMixin, JSONMixin):
"""Base class for UI classes implementing AJAX functionality.
The :py:class:`JSONMixin` provides the following attributes:
- ``ajax_required``
- ``content_type``
- ``decoder_class``
- ``encoder_class``
- ``json_required``
This mixin provides the following attributes:
- ``logging_enabled``
- ``pattern``
- ``ui``
"""
logging_enabled = False
"""Indicates logging is enabled for view activities."""
object = None
"""For views that operate on a single record, this is the record (model instance). See ``get_object()``."""
pattern_name = None
"""The pattern name of the view. Overrides the default."""
ui = None
"""The current ModelUI instance."""
# noinspection PyMethodMayBeStatic
[docs] def get_display_name(self, record):
"""Get the human-friendly name of a model instance.
:param record: The model instance.
:rtype: str
"""
try:
return record.get_display_name()
except AttributeError:
return str(record)
[docs] def get_object(self):
"""Get the record for the current request. Applies only to views that deal with one record at a time.
:rtype: BaseType[django.db.models.Model] | None
"""
lookup_field = self.ui.get_lookup_field()
try:
criteria = {lookup_field: self.args[0]}
except IndexError:
message = 'The "%s" lookup_field does not exist in args[0] for the "%s" view.'
log.debug(message % (lookup_field, self.__class__.__name__))
return None
try:
return self.ui.model.objects.get(**criteria)
except self.ui.model.DoesNotExist:
log.debug("Object not found: %s" % criteria)
return None
# Views
[docs]class UIAjaxAutoCompleteView(UIAjaxMixin, View):
"""Facilitate AJAX lookups for foreign keys and many to many fields.
.. note::
This differs from :py:class:`UIAjaxSearchView` in that the format of the search results is provided for use with
select2.
To invoke auto-complete, use the ``auto_complete_fields`` on the form options for the model that refers to
remote fields.
"""
case_sensitive = False
"""Indicates whether the search will be case sensitive."""
criteria = None
"""Additional criteria by which the results should be filtered."""
exact_matching = False
"""When ``True``, exact matching is required."""
fields = None
"""A list of field names on the model to be searched for results."""
label_field = None
"""The field to use for the label of each result. If omitted, ``get_choice_name()`` will be called. Failing that,
the string representation of the result is used.
"""
term_keyword = "term"
"""The GET keyword used to identify the search term."""
ui = None
"""The current ModelUI instance."""
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
term = request.GET.get(self.term_keyword, None)
criteria = self.get_criteria(request)
output = {
'results': list(),
}
qs = self.get_queryset(request, term)
for row in qs:
identifier = self.ui.get_identifier(row)
if not self.ui.check_permission(request, VERB.LIST, record=row):
continue
if self.label_field is not None:
label = getattr(row, self.label_field)
else:
try:
label = self.ui.get_display_value(row, as_choice=True, label_field=self.label_field)
# label = row.get_choice_name()
except AttributeError:
label = str(row)
output['results'].append({'id': identifier, 'text': label})
return self.get_json_response(output)
[docs] def get_criteria(self, request):
"""Get any additional criteria.
:param request: The current request instance.
:rtype: dict
"""
if self.criteria is not None:
criteria = self.criteria.copy()
else:
criteria = dict()
# criteria=key:value,key:value
if 'criteria' in request.GET:
pairs = request.GET.get("criteria").split(",")
for p in pairs:
key, value = p.split(":")
key = key.strip()
value = smart_cast(value.strip())
criteria[key] = value
return criteria
[docs] def get_queryset(self, request, term):
"""Get the records matching the given search term.
:rtype: django.db.models.QuerySet
"""
criteria = self.get_criteria(request)
if term is None:
return self.ui.get_queryset(self.request, criteria=criteria)
if self.exact_matching:
match = "exact"
else:
match = "contains"
if not self.case_sensitive:
match = "i%s" % match
or_condition = Q()
for field_name in self.fields:
lookup = "%s__%s" % (field_name, match)
_criteria = {lookup: term}
or_condition.add(Q(**_criteria), Q.OR)
qs = self.ui.model.objects.filter(or_condition)
if criteria:
qs = qs.filter(**criteria)
return qs
[docs]class UIAjaxChainedLookupView(UIAjaxMixin, View):
"""Provide results for a reference field (foreign key or many to many) based on the value of another selected
field.
The following steps are required to set up a chain:
**For This Example***
Imagine you have three models; Book, Part, and Chapter. A chapter is associated with both Book and Part. We want
the part drop-down to populate based on the selected book.
**Add Chain Options to Dependent Model**
Define ``ajax_chained_options`` for Part:
.. code-block:: python
class PartUI(ui.ModelUI):
model = Part
ajax_chained_lookup_options = ui.AjaxChainedLookupOptions("book", identifier="pk")
# ...
This sets up the AJAX view that responds to the chain request.
**Tell the Chapter Form to Use Chaining**
Next define the chain on ``form_options`` (or ``create_options`` and ``update_options``):
.. code-block:: python
class ChapterUI(ui.ModelUI):
model = Chapter
form_options = ui.FormOptions(
"book",
"part",
"title",
"body",
"description",
chained_fields=[ui.ChainedLookup("book", "part")],
)
The first field of ``ChainedLookup`` (``book``) is the field used to filter the dropdown. The second field is the
name of the dropdown field (``part``).
"""
criteria = None
"""Additional criteria, if any, to be applied to the lookup."""
remote_field_identifier = None
"""Used when the remote field is also a reference field."""
remote_field_name = None
"""The name of the field that provides the key to chained results."""
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
output = list()
# Add empty option in case the target field is not required. The template (chained_lookup.js) is responsible for
# removing this from the output if the target field IS required.
output.append({'': "---"})
# jquery.chained passes the name of the remote field and its value via GET. For example: "client=1234"
remote_field_value = request.GET.get(self.remote_field_name, "")
if len(remote_field_value) == 0:
return self.get_json_response(output)
# An identifier indicates that the remote field is itself a remote field. If so, the lookup needs to be handled
# accordingly.
if self.remote_field_identifier is not None:
# Cast pk fields by default. This won't work if the pk is not an AutoField.
if self.remote_field_identifier == "pk":
remote_field_value = int(remote_field_value)
lookup = "%s__%s" % (self.remote_field_name, self.remote_field_identifier)
criteria = {
lookup: remote_field_value,
}
else:
criteria = {self.remote_field_name: remote_field_value}
# Apply additional criteria.
if self.criteria is not None:
criteria.update(self.criteria)
# Get the queryset.
model = self.ui.model
qs = model.objects.filter(**criteria)
for row in qs:
if not self.ui.site.check_permission(model, request, VERB.LIST, record=row):
continue
if self.remote_field_identifier is not None:
identifier = getattr(row, self.remote_field_identifier)
else:
identifier = getattr(row, self.remote_field_name)
try:
label = row.get_choice_name()
except AttributeError:
label = str(row)
output.append({identifier: label})
return self.get_json_response(output)
[docs]class UIAjaxChooserView(UIAjaxMixin, View):
"""A view for selecting a foreign key via a modal."""
criteria = None
"""Additional criteria by which results should be filtered."""
fields = None
"""The fields to be included in the output. Note: ``pk`` is always included."""
template = None
"""The template to use when ``html`` is the requested ``output_format``."""
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
output_format = request.GET.get("output_format", "json")
_criteria = request.GET.get("criteria", None)
if _criteria:
criteria = self.criteria or dict()
for i in _criteria.split(","):
key, value = i.split(":")
criteria[key] = value
else:
criteria = self.criteria
qs = self.ui.get_queryset(request, criteria=criteria)
# Handle JSON output.
if output_format == "json":
data = dict()
data['records'] = list()
data['success'] = True
data['count'] = qs.count()
for row in qs:
if not self.ui.check_permission(request, VERB.LIST, record=row):
continue
record = {'pk': row.pk}
for f in self.fields:
record[f] = getattr(row, f)
data['records'].append(record)
return self.get_json_response(data)
# Handle HTML output.
controls = self.get_controls()
context = {
'columns': controls,
'output_format': output_format,
'records': list(),
}
for row in qs:
if not self.ui.check_permission(request, VERB.LIST, record=row):
continue
data = list()
for control in controls:
data.append(control.get_datum(row, request))
identifier = self.ui.get_identifier(row)
context['records'].append(Record(identifier, row, data=data))
return self.render_to_response(context, self.template)
[docs] def get_controls(self):
"""Get the controls used to display fields/columns.
:rtype: list[BaseControl]
"""
controls = list()
for field_name in self.fields:
control = self.ui.get_control(field_name)
if control is None:
message = "%s %s has requested a control for field that has not been defined: %s"
log.warning(message % (self.ui.meta.model_name, self.__class__.__name__, field_name))
continue
controls.append(control)
return controls
[docs] def render_to_response(self, context, template):
"""Render the current context.
:param context: The context variables to use for rendering.
:type context: dict
:param template: The template to use for rendering.
:type template: str
:rtype: TemplateResponse
"""
return TemplateResponse(self.request, template, context=context)
[docs]class UIAjaxCreateView(UIAjaxFormMixin, View):
"""A view for creating a record via AJAX."""
display_name = None
"""The field on the record to use as the human-friendly name in the form control for the created record."""
identifier = "pk"
"""The unique identifier of the record to be passed back as the value for the form control for the created
record.
"""
template_name_suffix = "_ajax_create"
"""The suffix of the template used to display the creation form."""
def get_verb(self):
return VERB.AJAX_CREATE
[docs]class UIAjaxDetailView(UIAjaxMixin, View):
"""Display model detail via AJAX."""
fields = None
object = None
output_format = "json"
template_name_suffix = "_ajax_detail"
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object is None:
message = _("The requested %s record could not be found." % self.ui.get_verbose_name())
return self.get_json_response({'error': message})
if self.output_format == "html":
fields = list()
controls = self.get_controls()
for control in controls:
fields.append(control.get_datum(self.object))
template = os.path.join("superdjango", "ui", "includes", "model_detail_table.html")
return self.render_to_response({'fields': fields}, template)
else:
output = list()
controls = self.get_controls()
for control in controls:
datum = control.get_datum(self.object)
output.append([datum.label, datum.display_value])
return self.get_json_response(output)
[docs] def get_controls(self):
"""Get the controls used to display fields/columns.
:rtype: list[BaseControl]
"""
controls = list()
for field_name in self.fields:
control = self.ui.get_control(field_name)
if control is None:
message = "%s %s has requested a control for field that has not been defined: %s"
log.warning(message % (self.ui.meta.model_name, self.__class__.__name__, field_name))
continue
controls.append(control)
return controls
[docs] def get_object(self):
"""Get the object (model instance) that may be used in :py:class:`CreateView`, :py:class:`DetailView`,
:py:class:`DeleteView`, and :py:class:`UpdateView`.
"""
lookup_field = self.ui.get_lookup_field()
try:
criteria = {lookup_field: self.args[0]}
except IndexError:
message = 'The "%s" lookup_field does not exist in args[0] for the "%s" view.'
log.debug(message % (lookup_field, self.__class__.__name__))
return None
try:
return self.ui.model.objects.get(**criteria)
except self.ui.model.DoesNotExist:
log.debug("Object not found: %s" % criteria)
return None
[docs] def render_to_response(self, context, template):
"""Render the current context.
:param context: The context variables to use for rendering.
:type context: dict
:param template: The template to use for rendering.
:type template: str
:rtype: TemplateResponse
"""
return TemplateResponse(self.request, template, context=context)
[docs]class UIAjaxDragAndDropView(UIAjaxMixin, View):
"""Support for AJAX drag and drop.
The ``hook_ajax_drag_and_drop`` is called after the column change and (optionally) sort order is applied to the
record, the function signature is ``hook(column_value, record, sort_order=sort_order)``.
"""
column_field = "stage"
""""The name of the field used to define Kanban columns. This may be a foreign key or a field with choices
defined.
"""
column_model = None
"""The model class of the foreign key field used to identify columns."""
sort_order_field = "sort_order"
"""The field to use for sorting a record within the ``column_field``."""
def get(self, request, *args, **kwargs):
# Get the column ID or column value.
column_model = self._get_column_model()
column_value = None
if column_model is not None:
column_id = request.GET.get("column_id", None)
if column_id:
try:
column_value = column_model.objects.get(pk=int(column_id))
except column_model.DoesNotExist:
return self.get_json_response({'error': "Column does not exist: %s" % column_id})
else:
column_value = request.GET.get("column_id", None)
if column_value is None:
return self.get_json_response(dict())
# Extract record IDs.
identifiers = request.GET.get("identifiers", None)
if identifiers is None:
return self.get_json_response(dict())
identifiers = identifiers.split(",")
# Update the column field and re-order the records, if supported.
sort_order = 1
for identifier in identifiers:
try:
record = self.ui.model.objects.get(pk=int(identifier))
except self.ui.model.DoesNotExist:
continue
setattr(record, self.column_field, column_value)
if self.ui.is_sort_model():
setattr(record, self.sort_order_field, sort_order)
sort_order += 1
for hook in hooks.get("hook_ajax_drag_and_drop"):
hook(column_value, record, sort_order=sort_order)
if self.ui.is_audit_model():
record.audit(request.user)
else:
record.save()
return self.get_json_response({'success': "Drag and drop finished."})
def _get_column_model(self):
"""Get the model class the column represents, which is ``None`` if the column is a choice field and not a
foreign key.
"""
if self.column_model is not None:
return self.column_model
# Instance of ForwardManyToOneDescriptor.
column_field = getattr(self.ui.model, self.column_field)
try:
return column_field.field.remote_field.model
except AttributeError:
return None
[docs]class UIAjaxListView(UIAjaxMixin, View):
"""AJAX list of records.
.. note::
This is currently only supporting the DataTable list type and is very specific to the output format required by
the JavaScript implementation.
"""
fields = None
link_field = None
link_to = None
minimum_length = 3
prefetch_related = None
queryset = None
search_fields = None
select_related = None
serializer = None
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
# from pprint import pprint
# pprint(request.GET)
queryset = self.get_queryset()
queryset = self.get_filtered_queryset(queryset)
filtered_records = queryset.count()
queryset = self.get_ordered_queryset(queryset)
queryset = self.get_paginated_queryset(queryset)
# noinspection PyProtectedMember
total_records = self.ui.model._default_manager.count()
output = {
"recordsTotal": total_records,
"recordsFiltered": filtered_records,
"data": list(),
}
controls = self.get_controls()
for row in queryset:
if not self.ui.check_permission(request, VERB.LIST, record=row):
continue
data = dict()
# identifier = self.ui.get_identifier(row)
for control in controls:
url = None
if self.link_field == control.name:
url = self.ui.get_url(self.link_to, record=row)
data[control.name] = control.get_datum(row, url=url).display_value
output['data'].append(data)
return self.get_json_response(output)
[docs] def get_controls(self):
"""Get the controls used to display fields/columns.
:rtype: list[BaseControl]
"""
controls = list()
for field_name in self.fields:
control = self.ui.get_control(field_name)
if control is None:
message = "%s %s has requested a control for field that has not been defined: %s"
log.warning(message % (self.ui.meta.model_name, self.__class__.__name__, field_name))
continue
controls.append(control)
return controls
def get_filtered_queryset(self, queryset):
if self.search_fields is None:
return queryset
term = self.request.GET.get('search[value]', "")
if len(term) < self.minimum_length:
return queryset
or_condition = Q()
for field_name, match in self.search_fields.items():
lookup = "%s__%s" % (field_name, match)
criteria = {lookup: term}
or_condition.add(Q(**criteria), Q.OR)
return queryset.filter(or_condition)
[docs] def get_queryset(self):
"""Get the queryset used by the view. This will either be a list or individual instance.
:rtype: django.db.models.QuerySet
"""
if self.queryset is not None:
# noinspection PyProtectedMember
return self.queryset._clone()
# noinspection PyProtectedMember
queryset = self.ui.model._default_manager.all()
if isinstance(self.select_related, (list, tuple)):
queryset = queryset.select_related(*self.select_related)
if isinstance(self.prefetch_related, (list, tuple)):
queryset = queryset.prefetch_related(*self.prefetch_related)
return queryset
def get_ordered_queryset(self, queryset):
# print(self.request.GET['order[0][column'])
# 'order[0][column]': ['1'], 'order[0][dir]': ['asc']
order_column_index = self.request.GET.get('order[0][column]', None)
if order_column_index is None:
return queryset
order_direction = self.request.GET.get('order[0][dir]')
order_by = self.fields[int(order_column_index)]
if order_direction == "desc":
order_by = "-" + order_by
return queryset.order_by(order_by)
def get_paginated_queryset(self, queryset):
limit = int(self.request.GET.get("length", "10"))
start = int(self.request.GET.get('start', "0"))
return queryset[start:limit + start]
[docs]class UIAjaxMarkCompleteView(UIAjaxMixin, View):
"""Mark a record complete."""
callback = None
"""A callable that overrides all processing. It must accept the model instance and request as arguments and return
the data (dict) to be processed through ``get_json_response()``. Data includes:
- heading: Optional. The heading, if any, for the toast displayed to the user.
- hideAfter: Required. The number of milliseconds to display the toast, or ``False`` to disable automatic fade out.
- position: Optional. The position of the toast.
- text: Required. The message to display.
Additionally (and obviously), the callback is expected to mark the record as complete.
"""
error_message = None
success_message = None
update_fields = None
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object is None:
message = _("The requested %s record could not be found." % self.ui.get_verbose_name())
return self.get_json_response({'heading': _("Error"), 'text': message, 'hideAfter': False})
if callable(self.callback):
data = self.callback(self.object, request)
return self.get_json_response(data)
self.object.mark_complete(request.user, commit=False)
if self.update_fields is not None:
for key, value in self.update_fields.items():
setattr(self.object, key, value)
if self.ui.behaviors.is_audit_model():
self.object.audit(request.user, commit=False)
self.object.save()
data = {
'heading': _("Completed"),
'text': self.success_message,
'hideAfter': 10000,
'position': "top-right",
}
return self.get_json_response(data)
[docs]class UIAjaxUpdateView(UIAjaxFormMixin, View):
"""A view for updating a record via AJAX."""
fields = None
"""A list of field names that may be used during record update."""
display_name = None
"""The field on the record to use as the human-friendly name in the form control for the updated record."""
identifier = "pk"
"""The unique identifier of the record to be passed back as the value for the form control for the updated
record.
"""
template_name_suffix = "_ajax_update"
"""The suffix of the template used to display the update form."""
[docs] def get(self, request, *args, **kwargs):
"""Get the HTML form for the modal."""
self.args = args
self.kwargs = kwargs
self.request = request
self.object = self.get_object()
form = self.ui.get_form(request, record=self.object)
context = self.get_context_data(form=form)
return self.render_to_response(context)
[docs] def get_object(self):
lookup_field = self.ui.get_lookup_field()
try:
criteria = {lookup_field: self.args[0]}
except IndexError:
message = 'The "%s" lookup_field does not exist in args for the "%s" view'
raise ImproperlyConfigured(message % (lookup_field, self.__class__.__name__))
return get_object_or_404(self.ui.model, **criteria)
def get_verb(self):
return VERB.AJAX_UPDATE
[docs] def post(self, request, *args, **kwargs):
"""Get the current model instance."""
self.args = args
self.kwargs = kwargs
self.request = request
self.object = self.get_object()
form = self.ui.get_form(request, data=request.POST, files=request.FILES, record=self.object)
if form.is_valid():
return self.form_valid(form)
return self.form_invalid(form)
class UIAjaxFileUploadView(UIAjaxMixin, View):
"""A UI view which supports uploading a file via AJAX."""
# https://simpleisbetterthancomplex.com/tutorial/2016/11/22/django-multiple-file-upload-using-ajax.html
pass
[docs]class UIAjaxReorderView(UIAjaxMixin, View):
sort_order_field = "sort_order"
# noinspection PyUnusedLocal
def get(self, request, *args, **kwargs):
# TODO: Ajax reordering is buggy and needs work. Testing should confirm reorder logic is working.
# Extract the record ids.
identifiers = request.GET.get("identifiers", None)
if identifiers is None:
return self.get_json_response(dict())
identifiers = identifiers.split(",")
# Get the actual records.
lookup_field = self.ui.get_lookup_field()
records = list()
relative_position = 1
for identifier in identifiers:
# Kind of a hack, but if the lookup field is a standard primary key, then we need to convert the identifier
# received from GET to an integer or the model lookup will fail.
if lookup_field == "pk":
identifier = int(identifier)
criteria = {
lookup_field: identifier,
}
try:
record = self.ui.model.objects.get(**criteria)
records.append((record, relative_position))
relative_position += 1
except self.ui.model.DoesNotExist:
continue
# Now re-order the records. If this is a sortable model, we use that functionality. Otherwise a rather naive
# re-sort is used.
if hasattr(self.ui.model, "update_sort_order") and callable(self.ui.model.update_sort_order):
for record, relative_position in records:
record.update_sort_order(record.sort_order + relative_position)
else:
default_sort_order = 1
for record, relative_position in records:
current_sort_order = getattr(record, self.sort_order_field, default_sort_order)
new_sort_order = current_sort_order + relative_position
setattr(record, self.sort_order_field, new_sort_order)
record.save()
data = {'success': "Reordered %s %s." % (len(records), self.ui.get_verbose_name_plural())}
return self.get_json_response(data)
[docs]class UIAjaxSearchView(UIAjaxMixin, View):
"""Facilitates model search via AJAX."""
case_sensitive = False
"""Indicates whether the search will be case sensitive."""
criteria = None
"""Additional criteria by which results should be filtered."""
exact_matching = False
"""When ``True``, exact matching is required."""
fields = None
"""A list of field names on the model to be searched for results."""
label_field = None
"""The field to use for the label of each result. If omitted, ``get_display_name()`` will be called. Failing that,
the string representation of the result is used.
"""
link_to = VERB.DETAIL
"""The name of the view (verb) to which search results should be linked."""
minimum_length = 3
"""The minimum number of characters required to generate a queryset."""
term_keyword = "term"
"""The GET keyword used to identify the search term."""
# noinspection PyUnusedLocal
[docs] def get(self, request, *args, **kwargs):
"""Get the search results."""
# Prepare for output.
output = dict()
# Get the term from GET and validate it.
term = request.GET.get(self.term_keyword, None)
if term is None:
output[0] = {
'label': _("GET term_keyword missing or has no value: %s" % self.term_keyword),
'url': "#",
}
return self.get_json_response(output)
if len(term) < self.minimum_length:
output[0] = {
'label': _("Enter %s or more characters." % self.minimum_length),
'url': "#",
}
return self.get_json_response(output)
# Search for records.
qs = self.get_queryset(term)
if qs.count() == 0:
output[0] = {
'label': _('No results for "%s".' % term),
'url': "#",
}
return self.get_json_response(output)
# Add records to the output.
for row in qs:
# Make sure the user is allowed to see (or otherwise do something) with the record.
if not self.ui.check_permission(request, self.link_to, record=row):
continue
# Get the record identifier (pk, uuid, etc.)
identifier = self.ui.get_identifier(row)
# Get the link for the found record.
url = self.ui.get_url(self.link_to, record=row)
if url is None:
url = "#"
# How the record is displayed in the output.
if self.label_field is not None:
label = getattr(row, self.label_field)
else:
label = self.get_display_name(row)
# Add the record to the output.
output[identifier] = {
'label': label,
'url': url,
}
return self.get_json_response(output)
[docs] def get_queryset(self, term):
"""Get the records matching the given search term.
:rtype: django.db.models.QuerySet
"""
match = "contains"
if self.exact_matching:
match = "exact"
if not self.case_sensitive:
match = "i%s" % match
or_condition = Q()
for field_name in self.fields:
lookup = "%s__%s" % (field_name, match)
criteria = {lookup: term}
or_condition.add(Q(**criteria), Q.OR)
qs = self.ui.model.objects.filter(or_condition)
if self.criteria is not None:
qs = qs.filter(**self.criteria)
return qs