from django import forms
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from dal.widgets import QuerySetSelectMixin, WidgetMixin
def _is_iterable(x):
try:
iter(x)
except TypeError:
return False
return True
# ---------------------------------------------------------------------------
# Queryset-backed widgets (FK / M2M)
# ---------------------------------------------------------------------------
[docs]
class ModelAlight(
QuerySetSelectMixin,
AlightWidgetMixin,
forms.Select,
):
"""Single-select autocomplete widget backed by a QuerySet."""
[docs]
class ModelAlightMultiple(
QuerySetSelectMixin,
AlightWidgetMixin,
forms.SelectMultiple,
):
"""Multi-select autocomplete widget backed by a QuerySet."""
# ---------------------------------------------------------------------------
# Non-queryset widgets (arbitrary choice lists)
# ---------------------------------------------------------------------------
[docs]
class Alight(WidgetMixin, AlightWidgetMixin, forms.Select):
"""Single-select autocomplete for arbitrary choices.
Without a ``url`` the component filters ``<option>`` elements locally in
JS — no server round-trip needed. With a ``url`` it fetches from the
view as usual.
"""
[docs]
class AlightMultiple(WidgetMixin, AlightWidgetMixin, forms.SelectMultiple):
"""Multiple-select autocomplete for arbitrary choices."""
[docs]
class ListAlight(WidgetMixin, AlightWidgetMixin, forms.Select):
"""Single-select autocomplete backed by ``AlightListView``.
Use alongside ``AlightListView`` on the server.
"""
# ---------------------------------------------------------------------------
# Tag widget
# ---------------------------------------------------------------------------
[docs]
class TagAlight(WidgetMixin, AlightWidgetMixin, forms.SelectMultiple):
"""Free-text tag widget — value stored as comma-separated text.
AlightInitialRenderMixin is intentionally omitted: tags are not PKs so
the queryset-filter approach would break; optgroups() handles initial
values directly via _iter_tag_values().
Tags are not backed by a model: the tag text IS the option value.
Use alongside ``AlightListView`` with a ``create()`` method, or any view
that returns HTML fragments.
The stored field value is a comma-separated string (same as TagSelect2).
"""
def option_value(self, value):
return value
def _iter_tag_values(self, value):
"""Yield individual tag strings from a raw value."""
if isinstance(value, str):
value = value.split(',')
for v in value:
if not v:
continue
yield self.option_value(str(v).strip())
[docs]
def optgroups(self, name, value, attrs=None):
default = (None, [], 0)
groups = [default]
for i, v in enumerate(self._iter_tag_values(value)):
default[1].append(self.create_option(v, v, v, True, i))
return groups
[docs]
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(data, files, name)
return ','.join(values)
[docs]
class TaggitAlight(TagAlight):
[docs]
def value_from_datadict(self, data, files, name):
value = super().value_from_datadict(data, files, name)
# trailing comma keeps multi-word single tags intact for taggit's parser
if value and ',' not in value:
value = '%s,' % value
return value
def option_value(self, value):
# taggit may yield TaggedItem objects on initial render
return value.tag.name if hasattr(value, 'tag') else value