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