Source code for dal.widgets
"""Autocomplete widgets bases."""
import copy
import json
from django import forms
from django.urls import reverse
from django.utils.safestring import mark_safe
from dal import forward
[docs]
class WidgetMixin(object):
"""Base mixin for autocomplete widgets.
.. py:attribute:: url
Absolute URL to the autocomplete view for the widget. It can be set to
a URL name, in which case it will be reversed when the attribute is
accessed.
.. py:attribute:: forward
List of field names to forward to the autocomplete view, useful to
filter results using values of other fields in the form.
Items of the list must be one of the following:
- string (e. g. "some_field"): forward a value from
the field with named "some_field";
- ``dal.forward.Field("some_field")``: the same as above;
- ``dal.forward.Field("some_field", "dst_field")``: forward a value from
the field with named "some_field" as "dst_field";
- ``dal.forward.Const("some_value", "dst_field")``: forward a constant
value "some_value" as "dst_field".
.. py:attribute:: autocomplete_function
Identifier of the javascript callback that should be
executed when such a widget is loaded in the DOM,
either on page load or dynamically.
"""
def __init__(self, url=None, forward=None, *args, **kwargs):
"""Instanciate a widget with a URL and a list of fields to forward."""
self.url = url
self.forward = list(forward) if forward else []
self.placeholder = (kwargs.get("attrs") or {}).get("data-placeholder")
super(WidgetMixin, self).__init__(*args, **kwargs)
def __deepcopy__(self, memo):
clone = super().__deepcopy__(memo)
clone.forward = self.forward.copy()
return clone
[docs]
def build_attrs(self, *args, **kwargs):
"""Build HTML attributes for the widget."""
attrs = super(WidgetMixin, self).build_attrs(*args, **kwargs)
if self.url is not None:
attrs['data-autocomplete-light-url'] = self.url
autocomplete_function = getattr(self, 'autocomplete_function', None)
if autocomplete_function:
attrs.setdefault('data-autocomplete-light-function',
autocomplete_function)
return attrs
[docs]
def filter_choices_to_render(self, selected_choices):
"""Filter choices to selected ones; inject values absent from the list."""
existing_keys = {str(c[0]) for c in self.choices}
self.choices = [c for c in self.choices if str(c[0]) in selected_choices]
self.choices += [(v, v) for v in selected_choices if v not in existing_keys]
@staticmethod
def _make_forward_dict(f):
"""Convert forward declaration to a dictionary.
A returned dictionary will be dumped to JSON while rendering widget.
"""
if isinstance(f, str):
return forward.Field(f).to_dict()
elif isinstance(f, forward.Forward):
return f.to_dict()
else:
raise TypeError("Cannot use {} as forwarded value".format(f))
[docs]
def render_forward_conf(self, id):
"""Render forward configuration for the field."""
if self.forward:
return \
'<div style="display:none" class="dal-forward-conf" ' + \
'id="dal-forward-conf-for_{id}"'.format(id=self.attrs.get("id", id)) + \
'>' \
'<script type="text/dal-forward-conf">' + \
json.dumps(
[self._make_forward_dict(f) for f in self.forward]
) + \
'</script>' \
'</div>'
else:
return ""
[docs]
def optgroups(self, name, value, attrs=None):
"""
Exclude unselected self.choices before calling the parent method.
Used by Django>=1.10.
"""
# Filter out None values, not needed for autocomplete
selected_choices = [str(c) for c in value if c]
all_choices = copy.copy(self.choices)
if self.url:
self.filter_choices_to_render(selected_choices)
elif not self.allow_multiple_selected:
if self.placeholder:
self.choices.insert(0, (None, ""))
result = super(WidgetMixin, self).optgroups(name, value, attrs)
self.choices = all_choices
return result
[docs]
def render(self, name, value, attrs=None, renderer=None, **kwargs):
"""Call Django render together with `render_forward_conf`."""
widget = super(WidgetMixin, self).render(name, value, attrs, **kwargs)
try:
field_id = attrs['id']
except (KeyError, TypeError):
field_id = name
conf = self.render_forward_conf(field_id)
return mark_safe(widget + conf)
def _get_url(self):
if self._url is None:
return None
if '/' in self._url:
return self._url
return reverse(self._url)
def _set_url(self, url):
self._url = url
url = property(_get_url, _set_url)
[docs]
class Select(WidgetMixin, forms.Select):
"""Replacement for Django's Select to render only selected choices."""
[docs]
class SelectMultiple(WidgetMixin, forms.SelectMultiple):
"""Replacement SelectMultiple to render only selected choices."""
[docs]
class QuerySetSelectMixin(WidgetMixin):
"""QuerySet support for choices."""
[docs]
def filter_choices_to_render(self, selected_choices):
"""Filter out un-selected choices if choices is a QuerySet."""
try:
self.choices.queryset = self.choices.queryset.filter(
pk__in=[c for c in selected_choices if c]
)
except ValueError:
# if selected_choices are invalid, do nothing
pass