"""Select2 view implementation."""
from collections import OrderedDict
from collections.abc import Sequence
from django import http
from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db.models import F
from django.utils.translation import gettext as _
from django.views.generic.list import View
from dal.views import BaseQuerySetView, ViewMixin
[docs]
class Select2ViewMixin(object):
"""View mixin to render a JSON response for Select2."""
[docs]
def get_results(self, context):
"""Return data for the 'results' key of the response."""
return [
{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in context['object_list']
]
[docs]
def get_create_option(self, context, q):
"""Form the correct create_option to append to results."""
if not self._should_show_create(context, q):
return []
return [{
'id': q,
'text': _('Create "%(new_value)s"') % {'new_value': q},
'create_id': True,
}]
[docs]
def render_to_response(self, context):
"""Return a JSON response in Select2 format."""
q = self.request.GET.get('q', None)
create_option = self.get_create_option(context, q)
return http.JsonResponse(
{
'results': self.get_results(context) + create_option,
'pagination': {
'more': self.has_more(context)
}
})
[docs]
class Select2QuerySetView(Select2ViewMixin, BaseQuerySetView):
"""List options for a Select2 widget."""
[docs]
def post(self, request, *args, **kwargs):
"""Create an object and return its id/text as JSON for Select2."""
try:
result = self._post(request)
except ValidationError as error:
msg = error.message_dict.get(self.create_field, str(error)) \
if hasattr(error, 'message_dict') else str(error)
return http.JsonResponse({'error': msg if isinstance(msg, str) else msg[0]})
if isinstance(result, http.HttpResponse):
return result
return http.JsonResponse({
'id': self.get_result_value(result),
'text': self.get_selected_result_label(result),
})
[docs]
class Select2GroupQuerySetView(Select2QuerySetView):
"""List of grouped options for a Select2 widget.
.. py:attribute:: group_by_related
Name of the field for the related Model on a One to Many relation
.. py:attribute:: related_field_name
Name of the related Model field to run filter against.
"""
group_by_related = None
related_field_name = 'name'
[docs]
def get_results(self, context):
"""Return the options grouped by a common related model.
Raises ImproperlyConfigured if self.group_by_name is not configured
"""
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
groups = OrderedDict()
object_list = context['object_list']
object_list = object_list.annotate(
group_name=F(f'{self.group_by_related}__{self.related_field_name}'))
for result in object_list:
group_name = getattr(result, 'group_name')
groups.setdefault(group_name, [])
groups[group_name].append(result)
return [{
'id': None,
'text': group,
'children': [{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in results]
} for group, results in groups.items()]
[docs]
class Select2ListView(ViewMixin, View):
"""Autocomplete from a list of items rather than a QuerySet."""
[docs]
def get_list(self):
"""Return the list strings from which to autocomplete."""
return []
[docs]
def get(self, request, *args, **kwargs):
"""Return option list json response."""
results = self.get_list()
create_option = []
if self.q:
results = self.autocomplete_results(results)
if hasattr(self, 'create'):
create_option = [{
'id': self.q,
'text': _('Create "%(new_value)s"') % {
'new_value': self.q
},
'create_id': True
}]
return http.JsonResponse({
'results': self.results(results) + create_option
}, content_type='application/json')
[docs]
def autocomplete_results(self, results):
"""Return list of strings that match the autocomplete query."""
if all(isinstance(el, list) for el in results) and len(results) > 0:
return [[x, y] for [x, y] in results if self.q.lower() in y.lower()]
if all(isinstance(el, tuple) for el in results) and len(results) > 0:
return [[x, y] for (x, y) in results if self.q.lower() in y.lower()]
else:
return [x for x in results if self.q.lower() in x.lower()]
[docs]
def results(self, results):
"""Return the result dictionary."""
if all(isinstance(el, list) for el in results) and len(results) > 0:
return [dict(id=x, text=y) for [x, y] in results]
elif all(isinstance(el, tuple) for el in results) and len(results) > 0:
return [dict(id=x, text=y) for (x, y) in results]
else:
return [dict(id=x, text=x) for x in results]
[docs]
def post(self, request, *args, **kwargs):
"""Add an option to the autocomplete list.
If 'text' is not defined in POST or self.create(text) fails, raises
bad request. Raises ImproperlyConfigured if self.create if not defined.
"""
if not hasattr(self, 'create'):
raise ImproperlyConfigured('Missing "create()"')
text = request.POST.get('text', None)
if text is None:
return http.HttpResponseBadRequest()
text = self.create(text)
if text is None:
return http.HttpResponseBadRequest()
return http.JsonResponse({
'id': text,
'text': text,
})
[docs]
class Select2GroupListView(Select2ListView):
"""View mixin for grouped options."""
[docs]
def get_item_as_group(self, entry):
"""Return the item with its group."""
group = None
item = entry
if isinstance(entry, Sequence) and not isinstance(entry, str):
entry_length = len(entry)
if all(isinstance(el, list) for el in entry) and entry_length > 1:
group, item = entry[0:2]
return (group, item),
elif all(isinstance(el, list) for el in entry) and entry_length > 1:
group, item = entry[0:2]
return (group, item),
else:
if entry_length > 1:
group, item = entry[0:2]
elif entry_length > 0:
item = entry[0]
if not isinstance(item, Sequence) or isinstance(item, str):
item = (item,)
return (group, item),
[docs]
def get(self, request, *args, **kwargs):
"""Return option list with children(s) json response."""
results_dict = {}
results = self.get_list()
if results:
if (
all(isinstance(el, list) for el in results)
or all(isinstance(el, tuple) for el in results)
):
flat_results = [
(group[0], group[1], item[0], item[1]) for entry in results
for group, items in self.get_item_as_group(entry)
for item in items
]
if self.q:
q = self.q.lower()
flat_results = [(g, h, x, y) for g, h, x, y in flat_results
if q in y.lower()]
for group_id, group, item_id, item in flat_results:
results_dict.setdefault((group_id, group), [])
results_dict[(group_id, group)].append([item_id, item])
return http.JsonResponse({
"results": [
{
"id": x, "text": y
} for x, y in results_dict.pop((None, None), [])
] + [
{
"id": g[0],
"text": g[1],
"children": [
{"id": x, "text": y} for x, y in items
]
}
for g, items in results_dict.items()
]
})
else:
flat_results = [(group, item) for entry in results
for group, items in self.get_item_as_group(entry)
for item in items]
if self.q:
q = self.q.lower()
flat_results = [(g, x) for g, x in flat_results
if q in x.lower()]
for group, item in flat_results:
results_dict.setdefault(group, [])
results_dict[group].append(item)
return http.JsonResponse({
"results": [
{"id": x, "text": x} for x in results_dict.pop(None, [])
] + [
{
"id": g,
"text": g,
"children": [
{"id": x, "text": x} for x in items
]
}
for g, items in results_dict.items()
]
})