1import re
2
3from django.core.exceptions import ValidationError
4from django.forms.utils import RenderableFieldMixin, pretty_name
5from django.forms.widgets import MultiWidget, Textarea, TextInput
6from django.utils.functional import cached_property
7from django.utils.html import format_html, html_safe
8from django.utils.translation import gettext_lazy as _
9
10__all__ = ("BoundField",)
11
12
13class BoundField(RenderableFieldMixin):
14 "A Field plus data"
15
16 def __init__(self, form, field, name):
17 self.form = form
18 self.field = field
19 self.name = name
20 self.html_name = form.add_prefix(name)
21 self.html_initial_name = form.add_initial_prefix(name)
22 self.html_initial_id = form.add_initial_prefix(self.auto_id)
23 if self.field.label is None:
24 self.label = pretty_name(name)
25 else:
26 self.label = self.field.label
27 self.help_text = field.help_text or ""
28 self.renderer = form.renderer
29
30 @cached_property
31 def subwidgets(self):
32 """
33 Most widgets yield a single subwidget, but others like RadioSelect and
34 CheckboxSelectMultiple produce one subwidget for each choice.
35
36 This property is cached so that only one database query occurs when
37 rendering ModelChoiceFields.
38 """
39 id_ = self.field.widget.attrs.get("id") or self.auto_id
40 attrs = {"id": id_} if id_ else {}
41 attrs = self.build_widget_attrs(attrs)
42 return [
43 BoundWidget(self.field.widget, widget, self.form.renderer)
44 for widget in self.field.widget.subwidgets(
45 self.html_name, self.value(), attrs=attrs
46 )
47 ]
48
49 def __bool__(self):
50 # BoundField evaluates to True even if it doesn't have subwidgets.
51 return True
52
53 def __iter__(self):
54 return iter(self.subwidgets)
55
56 def __len__(self):
57 return len(self.subwidgets)
58
59 def __getitem__(self, idx):
60 # Prevent unnecessary reevaluation when accessing BoundField's attrs
61 # from templates.
62 if not isinstance(idx, (int, slice)):
63 raise TypeError(
64 "BoundField indices must be integers or slices, not %s."
65 % type(idx).__name__
66 )
67 return self.subwidgets[idx]
68
69 @property
70 def errors(self):
71 """
72 Return an ErrorList (empty if there are no errors) for this field.
73 """
74 return self.form.errors.get(
75 self.name, self.form.error_class(renderer=self.form.renderer)
76 )
77
78 @property
79 def template_name(self):
80 return self.field.template_name or self.form.renderer.field_template_name
81
82 def get_context(self):
83 return {"field": self}
84
85 def as_widget(self, widget=None, attrs=None, only_initial=False):
86 """
87 Render the field by rendering the passed widget, adding any HTML
88 attributes passed as attrs. If a widget isn't specified, use the
89 field's default widget.
90 """
91 widget = widget or self.field.widget
92 if self.field.localize:
93 widget.is_localized = True
94 attrs = attrs or {}
95 attrs = self.build_widget_attrs(attrs, widget)
96 if self.auto_id and "id" not in widget.attrs:
97 attrs.setdefault(
98 "id", self.html_initial_id if only_initial else self.auto_id
99 )
100 if only_initial and self.html_initial_name in self.form.data:
101 # Propagate the hidden initial value.
102 value = self.form._widget_data_value(
103 self.field.hidden_widget(),
104 self.html_initial_name,
105 )
106 else:
107 value = self.value()
108 return widget.render(
109 name=self.html_initial_name if only_initial else self.html_name,
110 value=value,
111 attrs=attrs,
112 renderer=self.form.renderer,
113 )
114
115 def as_text(self, attrs=None, **kwargs):
116 """
117 Return a string of HTML for representing this as an <input type="text">.
118 """
119 return self.as_widget(TextInput(), attrs, **kwargs)
120
121 def as_textarea(self, attrs=None, **kwargs):
122 """Return a string of HTML for representing this as a <textarea>."""
123 return self.as_widget(Textarea(), attrs, **kwargs)
124
125 def as_hidden(self, attrs=None, **kwargs):
126 """
127 Return a string of HTML for representing this as an <input type="hidden">.
128 """
129 return self.as_widget(self.field.hidden_widget(), attrs, **kwargs)
130
131 @property
132 def data(self):
133 """
134 Return the data for this BoundField, or None if it wasn't given.
135 """
136 return self.form._widget_data_value(self.field.widget, self.html_name)
137
138 def value(self):
139 """
140 Return the value for this BoundField, using the initial value if
141 the form is not bound or the data otherwise.
142 """
143 data = self.initial
144 if self.form.is_bound:
145 data = self.field.bound_data(self.data, data)
146 return self.field.prepare_value(data)
147
148 def _has_changed(self):
149 field = self.field
150 if field.show_hidden_initial:
151 hidden_widget = field.hidden_widget()
152 initial_value = self.form._widget_data_value(
153 hidden_widget,
154 self.html_initial_name,
155 )
156 try:
157 initial_value = field.to_python(initial_value)
158 except ValidationError:
159 # Always assume data has changed if validation fails.
160 return True
161 else:
162 initial_value = self.initial
163 return field.has_changed(initial_value, self.data)
164
165 def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
166 """
167 Wrap the given contents in a <label>, if the field has an ID attribute.
168 contents should be mark_safe'd to avoid HTML escaping. If contents
169 aren't given, use the field's HTML-escaped label.
170
171 If attrs are given, use them as HTML attributes on the <label> tag.
172
173 label_suffix overrides the form's label_suffix.
174 """
175 contents = contents or self.label
176 if label_suffix is None:
177 label_suffix = (
178 self.field.label_suffix
179 if self.field.label_suffix is not None
180 else self.form.label_suffix
181 )
182 # Only add the suffix if the label does not end in punctuation.
183 # Translators: If found as last label character, these punctuation
184 # characters will prevent the default label_suffix to be appended to the label
185 if label_suffix and contents and contents[-1] not in _(":?.!"):
186 contents = format_html("{}{}", contents, label_suffix)
187 widget = self.field.widget
188 id_ = widget.attrs.get("id") or self.auto_id
189 if id_:
190 id_for_label = widget.id_for_label(id_)
191 if id_for_label:
192 attrs = {**(attrs or {}), "for": id_for_label}
193 if self.field.required and hasattr(self.form, "required_css_class"):
194 attrs = attrs or {}
195 if "class" in attrs:
196 attrs["class"] += " " + self.form.required_css_class
197 else:
198 attrs["class"] = self.form.required_css_class
199 context = {
200 "field": self,
201 "label": contents,
202 "attrs": attrs,
203 "use_tag": bool(id_),
204 "tag": tag or "label",
205 }
206 return self.form.render(self.form.template_name_label, context)
207
208 def legend_tag(self, contents=None, attrs=None, label_suffix=None):
209 """
210 Wrap the given contents in a <legend>, if the field has an ID
211 attribute. Contents should be mark_safe'd to avoid HTML escaping. If
212 contents aren't given, use the field's HTML-escaped label.
213
214 If attrs are given, use them as HTML attributes on the <legend> tag.
215
216 label_suffix overrides the form's label_suffix.
217 """
218 return self.label_tag(contents, attrs, label_suffix, tag="legend")
219
220 def css_classes(self, extra_classes=None):
221 """
222 Return a string of space-separated CSS classes for this field.
223 """
224 if hasattr(extra_classes, "split"):
225 extra_classes = extra_classes.split()
226 extra_classes = set(extra_classes or [])
227 if self.errors and hasattr(self.form, "error_css_class"):
228 extra_classes.add(self.form.error_css_class)
229 if self.field.required and hasattr(self.form, "required_css_class"):
230 extra_classes.add(self.form.required_css_class)
231 return " ".join(extra_classes)
232
233 @property
234 def is_hidden(self):
235 """Return True if this BoundField's widget is hidden."""
236 return self.field.widget.is_hidden
237
238 @property
239 def auto_id(self):
240 """
241 Calculate and return the ID attribute for this BoundField, if the
242 associated Form has specified auto_id. Return an empty string otherwise.
243 """
244 auto_id = self.form.auto_id # Boolean or string
245 if auto_id and "%s" in str(auto_id):
246 return auto_id % self.html_name
247 elif auto_id:
248 return self.html_name
249 return ""
250
251 @property
252 def id_for_label(self):
253 """
254 Wrapper around the field widget's `id_for_label` method.
255 Useful, for example, for focusing on this field regardless of whether
256 it has a single widget or a MultiWidget.
257 """
258 widget = self.field.widget
259 id_ = widget.attrs.get("id") or self.auto_id
260 return widget.id_for_label(id_)
261
262 @cached_property
263 def initial(self):
264 return self.form.get_initial_for_field(self.field, self.name)
265
266 def build_widget_attrs(self, attrs, widget=None):
267 widget = widget or self.field.widget
268 attrs = dict(attrs) # Copy attrs to avoid modifying the argument.
269 if (
270 widget.use_required_attribute(self.initial)
271 and self.field.required
272 and self.form.use_required_attribute
273 ):
274 # MultiValueField has require_all_fields: if False, fall back
275 # on subfields.
276 if (
277 hasattr(self.field, "require_all_fields")
278 and not self.field.require_all_fields
279 and isinstance(self.field.widget, MultiWidget)
280 ):
281 for subfield, subwidget in zip(self.field.fields, widget.widgets):
282 subwidget.attrs["required"] = (
283 subwidget.use_required_attribute(self.initial)
284 and subfield.required
285 )
286 else:
287 attrs["required"] = True
288 if self.field.disabled:
289 attrs["disabled"] = True
290 if not widget.is_hidden and self.errors:
291 attrs["aria-invalid"] = "true"
292 # Preserve aria-describedby provided by the attrs argument so user
293 # can set the desired order.
294 if not attrs.get("aria-describedby") and not self.use_fieldset:
295 if aria_describedby := self.aria_describedby:
296 attrs["aria-describedby"] = aria_describedby
297 return attrs
298
299 @property
300 def aria_describedby(self):
301 # Preserve aria-describedby set on the widget.
302 if self.field.widget.attrs.get("aria-describedby"):
303 return None
304 aria_describedby = []
305 if self.auto_id and not self.is_hidden:
306 if self.help_text:
307 aria_describedby.append(f"{self.auto_id}_helptext")
308 if self.errors:
309 aria_describedby.append(f"{self.auto_id}_error")
310 return " ".join(aria_describedby)
311
312 @property
313 def widget_type(self):
314 return re.sub(
315 r"widget$|input$", "", self.field.widget.__class__.__name__.lower()
316 )
317
318 @property
319 def use_fieldset(self):
320 """
321 Return the value of this BoundField widget's use_fieldset attribute.
322 """
323 return self.field.widget.use_fieldset
324
325
326@html_safe
327class BoundWidget:
328 """
329 A container class used for iterating over widgets. This is useful for
330 widgets that have choices. For example, the following can be used in a
331 template:
332
333 {% for radio in myform.beatles %}
334 <label for="{{ radio.id_for_label }}">
335 {{ radio.choice_label }}
336 <span class="radio">{{ radio.tag }}</span>
337 </label>
338 {% endfor %}
339 """
340
341 def __init__(self, parent_widget, data, renderer):
342 self.parent_widget = parent_widget
343 self.data = data
344 self.renderer = renderer
345
346 def __str__(self):
347 return self.tag(wrap_label=True)
348
349 def tag(self, wrap_label=False):
350 context = {"widget": {**self.data, "wrap_label": wrap_label}}
351 return self.parent_widget._render(self.template_name, context, self.renderer)
352
353 @property
354 def template_name(self):
355 if "template_name" in self.data:
356 return self.data["template_name"]
357 return self.parent_widget.template_name
358
359 @property
360 def id_for_label(self):
361 return self.data["attrs"].get("id")
362
363 @property
364 def choice_label(self):
365 return self.data["label"]