Files
postgres-web/dep/django-selectable/selectable/forms/widgets.py
Magnus Hagander 6ffc1d4811 Import newer version of django-selectable
Sync up to the same version we have on the commitfest app, which will
also be required for eventual django 1.11 support.
2018-03-09 16:43:49 -05:00

329 lines
12 KiB
Python

from __future__ import unicode_literals
import inspect
import json
from django import forms
from django.conf import settings
from django.forms.utils import flatatt
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from selectable import __version__
from selectable.forms.base import import_lookup_class
__all__ = (
'AutoCompleteWidget',
'AutoCompleteSelectWidget',
'AutoComboboxWidget',
'AutoComboboxSelectWidget',
'AutoCompleteSelectMultipleWidget',
'AutoComboboxSelectMultipleWidget',
)
STATIC_PREFIX = '%sselectable/' % settings.STATIC_URL
class SelectableMediaMixin(object):
class Media(object):
css = {
'all': ('%scss/dj.selectable.css?v=%s' % (STATIC_PREFIX, __version__),)
}
js = ('%sjs/jquery.dj.selectable.js?v=%s' % (STATIC_PREFIX, __version__),)
new_style_build_attrs = (
'base_attrs' in
inspect.getargs(forms.widgets.Widget.build_attrs.__code__).args)
class BuildAttrsCompat(object):
"""
Mixin to provide compatibility between old and new function
signatures for Widget.build_attrs, and a hook for adding our
own attributes.
"""
# These are build_attrs definitions that make it easier for
# us to override, without having to worry about the signature,
# by adding a standard hook, `build_attrs_extra`.
# It has a different signature when we are running different Django
# versions.
if new_style_build_attrs:
def build_attrs(self, base_attrs, extra_attrs=None):
attrs = super(BuildAttrsCompat, self).build_attrs(
base_attrs, extra_attrs=extra_attrs)
return self.build_attrs_extra(attrs)
else:
def build_attrs(self, extra_attrs=None, **kwargs):
attrs = super(BuildAttrsCompat, self).build_attrs(
extra_attrs=extra_attrs, **kwargs)
return self.build_attrs_extra(attrs)
def build_attrs_extra(self, attrs):
# Default implementation, does nothing
return attrs
# These provide a standard interface for when we want to call build_attrs
# in our own `render` methods. In both cases it is the same as the Django
# 1.11 signature, but has a different implementation for different Django
# versions.
if new_style_build_attrs:
def build_attrs_compat(self, base_attrs, extra_attrs=None):
return self.build_attrs(base_attrs, extra_attrs=extra_attrs)
else:
def build_attrs_compat(self, base_attrs, extra_attrs=None):
# Implementation copied from Django 1.11, plus include our
# hook `build_attrs_extra`
attrs = base_attrs.copy()
if extra_attrs is not None:
attrs.update(extra_attrs)
return self.build_attrs_extra(attrs)
CompatMixin = BuildAttrsCompat
class AutoCompleteWidget(CompatMixin, forms.TextInput, SelectableMediaMixin):
def __init__(self, lookup_class, *args, **kwargs):
self.lookup_class = import_lookup_class(lookup_class)
self.allow_new = kwargs.pop('allow_new', False)
self.qs = kwargs.pop('query_params', {})
self.limit = kwargs.pop('limit', None)
super(AutoCompleteWidget, self).__init__(*args, **kwargs)
def update_query_parameters(self, qs_dict):
self.qs.update(qs_dict)
def build_attrs_extra(self, attrs):
attrs = super(AutoCompleteWidget, self).build_attrs_extra(attrs)
url = self.lookup_class.url()
if self.limit and 'limit' not in self.qs:
self.qs['limit'] = self.limit
if self.qs:
url = '%s?%s' % (url, urlencode(self.qs))
if 'data-selectable-options' in attrs:
attrs['data-selectable-options'] = json.dumps(attrs['data-selectable-options'])
attrs['data-selectable-url'] = url
attrs['data-selectable-type'] = 'text'
attrs['data-selectable-allow-new'] = str(self.allow_new).lower()
return attrs
class SelectableMultiWidget(CompatMixin, forms.MultiWidget):
def update_query_parameters(self, qs_dict):
self.widgets[0].update_query_parameters(qs_dict)
def decompress(self, value):
if value:
lookup = self.lookup_class()
model = getattr(self.lookup_class, 'model', None)
if model and isinstance(value, model):
item = value
value = lookup.get_item_id(item)
else:
item = lookup.get_item(value)
item_value = lookup.get_item_value(item)
return [item_value, value]
return [None, None]
def get_compatible_postdata(self, data, name):
"""Get postdata built for a normal <select> element.
Django MultiWidgets create post variables like ``foo_0`` and ``foo_1``,
and this behavior is not cleanly overridable. Non-multiwidgets, like
Select, get simple names like ``foo``. In order to keep this widget
compatible with requests designed for traditional select widgets,
search postdata for a name like ``foo`` and return that value.
This will return ``None`` if a ``<select>``-compatibile post variable
is not found.
"""
return data.get(name, None)
class _BaseSingleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
"""
Common base class for AutoCompleteSelectWidget and AutoComboboxSelectWidget
each which use one widget (primary_widget) to select text and a single
hidden input to hold the selected id.
"""
primary_widget = None
def __init__(self, lookup_class, *args, **kwargs):
self.lookup_class = import_lookup_class(lookup_class)
self.allow_new = kwargs.pop('allow_new', False)
self.limit = kwargs.pop('limit', None)
query_params = kwargs.pop('query_params', {})
widgets = [
self.primary_widget(
lookup_class, allow_new=self.allow_new,
limit=self.limit, query_params=query_params,
attrs=kwargs.get('attrs'),
),
forms.HiddenInput(attrs={'data-selectable-type': 'hidden'})
]
super(_BaseSingleSelectWidget, self).__init__(widgets, *args, **kwargs)
def value_from_datadict(self, data, files, name):
value = super(_BaseSingleSelectWidget, self).value_from_datadict(data, files, name)
if not value[1]:
compatible_postdata = self.get_compatible_postdata(data, name)
if compatible_postdata:
value[1] = compatible_postdata
if not self.allow_new:
return value[1]
return value
class AutoCompleteSelectWidget(_BaseSingleSelectWidget):
primary_widget = AutoCompleteWidget
class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin):
def build_attrs_extra(self, attrs):
attrs = super(AutoComboboxWidget, self).build_attrs_extra(attrs)
attrs['data-selectable-type'] = 'combobox'
return attrs
class AutoComboboxSelectWidget(_BaseSingleSelectWidget):
primary_widget = AutoComboboxWidget
class LookupMultipleHiddenInput(CompatMixin, forms.MultipleHiddenInput):
def __init__(self, lookup_class, *args, **kwargs):
self.lookup_class = import_lookup_class(lookup_class)
super(LookupMultipleHiddenInput, self).__init__(*args, **kwargs)
# This supports Django 1.11 and later
def get_context(self, name, value, attrs):
lookup = self.lookup_class()
values = self._normalize_value(value)
values = list(values) # force evaluation
context = super(LookupMultipleHiddenInput, self).get_context(name, values, attrs)
# Now override/add to what super() did:
subwidgets = context['widget']['subwidgets']
for widget_ctx, item in zip(subwidgets, values):
input_value, title = self._lookup_value_and_title(lookup, item)
widget_ctx['value'] = input_value # override what super() did
if title:
widget_ctx['attrs']['title'] = title
return context
# This supports Django 1.10 and earlier
def render(self, name, value, attrs=None, choices=()):
lookup = self.lookup_class()
value = self._normalize_value(value)
base_attrs = dict(self.attrs, type=self.input_type, name=name)
combined_attrs = self.build_attrs_compat(base_attrs, attrs)
id_ = combined_attrs.get('id', None)
inputs = []
for i, v in enumerate(value):
input_attrs = combined_attrs.copy()
v_, title = self._lookup_value_and_title(lookup, v)
input_attrs.update(
value=v_,
title=title,
)
if id_:
# An ID attribute was given. Add a numeric index as a suffix
# so that the inputs don't all have the same ID attribute.
input_attrs['id'] = '%s_%s' % (id_, i)
inputs.append('<input%s />' % flatatt(input_attrs))
return mark_safe('\n'.join(inputs))
# These are used by both paths
def build_attrs_extra(self, attrs):
attrs = super(LookupMultipleHiddenInput, self).build_attrs_extra(attrs)
attrs['data-selectable-type'] = 'hidden-multiple'
return attrs
def _normalize_value(self, value):
if value is None:
value = []
return value
def _lookup_value_and_title(self, lookup, v):
model = getattr(self.lookup_class, 'model', None)
item = None
if model and isinstance(v, model):
item = v
v = lookup.get_item_id(item)
title = None
if v:
item = item or lookup.get_item(v)
title = lookup.get_item_value(item)
return force_text(v), title
class _BaseMultipleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
"""
Common base class for AutoCompleteSelectMultipleWidget and AutoComboboxSelectMultipleWidget
each which use one widget (primary_widget) to select text and a multiple
hidden inputs to hold the selected ids.
"""
primary_widget = None
def __init__(self, lookup_class, *args, **kwargs):
self.lookup_class = import_lookup_class(lookup_class)
self.limit = kwargs.pop('limit', None)
position = kwargs.pop('position', 'bottom')
attrs = {
'data-selectable-multiple': 'true',
'data-selectable-position': position
}
attrs.update(kwargs.get('attrs', {}))
query_params = kwargs.pop('query_params', {})
widgets = [
self.primary_widget(
lookup_class, allow_new=False,
limit=self.limit, query_params=query_params, attrs=attrs
),
LookupMultipleHiddenInput(lookup_class)
]
super(_BaseMultipleSelectWidget, self).__init__(widgets, *args, **kwargs)
def value_from_datadict(self, data, files, name):
value = self.widgets[1].value_from_datadict(data, files, name + '_1')
if not value:
# Fall back to the compatible POST name
value = self.get_compatible_postdata(data, name)
return value
def build_attrs_extra(self, attrs):
attrs = super(_BaseMultipleSelectWidget, self).build_attrs_extra(attrs)
if 'required' in attrs:
attrs.pop('required')
return attrs
def render(self, name, value, attrs=None):
if value and not hasattr(value, '__iter__'):
value = [value]
value = ['', value]
return super(_BaseMultipleSelectWidget, self).render(name, value, attrs)
class AutoCompleteSelectMultipleWidget(_BaseMultipleSelectWidget):
primary_widget = AutoCompleteWidget
class AutoComboboxSelectMultipleWidget(_BaseMultipleSelectWidget):
primary_widget = AutoComboboxWidget