from __future__ import annotations
from typing import Any
from collections.abc import Mapping
from django.core.files.uploadedfile import UploadedFile
from django.forms.renderers import BaseRenderer
from django.forms.widgets import CheckboxInput, Media, Widget
from django.utils.datastructures import MultiValueDict
from django.utils.html import conditional_escape
from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext_lazy as _
class ClearableWidgetWrapper(Widget):
"""
Wraps another widget adding a clear checkbox, making it possible to
reset the field to some empty value even if the original input doesn't
have means to.
Useful for ``TextInput`` and ``Textarea`` based widgets used in combination
with nullable text fields.
Use it in ``Field.formfield`` or ``ModelAdmin.formfield_for_dbfield``:
field.widget = ClearableWidgetWrapper(field.widget)
``None`` is assumed to be a proper choice for the empty value, but you may
pass another one to the constructor.
"""
clear_checkbox_label = _("None")
template = '{0} {2} {3}'
# TODO: Label would be proper, but admin applies some hardly undoable
# styling to labels.
# template = '{} {}'
class Media:
js = ("modeltranslation/js/clearable_inputs.js",)
def __init__(self, widget: Widget, empty_value: Any | None = None) -> None:
"""
Remebers the widget we are wrapping and precreates a checkbox input.
Allows overriding the empty value.
"""
self.widget = widget
self.checkbox = CheckboxInput(attrs={"tabindex": "-1"})
self.empty_value = empty_value
def __getattr__(self, name: str) -> Any:
"""
If we don't have a property or a method, chances are the wrapped
widget does.
"""
if name != "widget":
return getattr(self.widget, name)
raise AttributeError
@property
def media(self):
"""
Combines media of both components and adds a small script that unchecks
the clear box, when a value in any wrapped input is modified.
"""
return self.widget.media + self.checkbox.media + Media(self.Media)
def render(
self,
name: str,
value: Any,
attrs: dict[str, Any] | None = None,
renderer: BaseRenderer | None = None,
) -> SafeString:
"""
Appends a checkbox for clearing the value (that is, setting the field
with the ``empty_value``).
"""
wrapped = self.widget.render(name, value, attrs, renderer)
checkbox_name = self.clear_checkbox_name(name)
checkbox_id = self.clear_checkbox_id(checkbox_name)
checkbox_label = self.clear_checkbox_label
checkbox = self.checkbox.render(
checkbox_name, value == self.empty_value, attrs={"id": checkbox_id}, renderer=renderer
)
return mark_safe(
self.template.format(
conditional_escape(wrapped),
conditional_escape(checkbox_id),
conditional_escape(checkbox_label),
conditional_escape(checkbox),
)
)
def value_from_datadict(
self, data: Mapping[str, Any], files: MultiValueDict[str, UploadedFile], name: str
) -> Any:
"""
If the clear checkbox is checked returns the configured empty value,
completely ignoring the original input.
"""
clear = self.checkbox.value_from_datadict(data, files, self.clear_checkbox_name(name))
if clear:
return self.empty_value
return self.widget.value_from_datadict(data, files, name)
def clear_checkbox_name(self, name: str) -> str:
"""
Given the name of the input, returns the name of the clear checkbox.
"""
return name + "-clear"
def clear_checkbox_id(self, name: str) -> str:
"""
Given the name of the clear checkbox input, returns the HTML id for it.
"""
return name + "_id"