Source code for dal.widgets
"""Autocomplete widgets bases."""
import copy
import json
from dal import forward
from django import VERSION
from django import forms
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
[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 = forward or []
self.placeholder = kwargs.get("attrs", {}).get("data-placeholder")
super(WidgetMixin, self).__init__(*args, **kwargs)
[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):
"""Replace self.choices with selected_choices."""
self.choices = [c for c in self.choices if
str(c[0]) in selected_choices]
@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=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 render_options(self, *args):
"""
Django-compatibility method for option rendering.
Should only render selected options, by setting self.choices before
calling the parent method.
Remove this code when dropping support for Django<1.10.
"""
selected_choices_arg = 1 if VERSION < (1, 10) else 0
# Filter out None values, not needed for autocomplete
selected_choices = [str(c) for c
in args[selected_choices_arg] 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, ""))
html = super(WidgetMixin, self).render_options(*args)
self.choices = all_choices
return html
[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