Source code for next.forms.serializers

"""Frozen-dataclass specs for rendering Django forms in custom templates."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Literal

from django import forms as django_forms
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper


if TYPE_CHECKING:
    from collections.abc import Iterable, Mapping, Sequence

    from django.forms import BaseForm, BoundField
    from django.forms.formsets import BaseFormSet


FieldKind = Literal["textarea", "checkbox", "select", "select_multi", "input"]


[docs] @dataclass(frozen=True, slots=True) class FieldSpec: """Render-time descriptor for one `BoundField`.""" bound: BoundField kind: FieldKind input_type: str value: Any selected: tuple[str, ...] is_extra: bool
[docs] @dataclass(frozen=True, slots=True) class FormsetRowSpec: """One row inside a `FormsetSpec`. Render `hidden_html` with `|safe`.""" fields: tuple[FieldSpec, ...] hidden_html: str delete_field: BoundField | None errors: Mapping[str, list[str]] is_extra: bool
[docs] @dataclass(frozen=True, slots=True) class FormsetSpec: """Template-friendly view of a Django formset (inline or standalone).""" prefix: str verbose_name_plural: str management_form: BaseForm rows: tuple[FormsetRowSpec, ...] non_form_errors: tuple[str, ...] can_delete: bool
[docs] @dataclass(frozen=True, slots=True) class FormSectionSpec: """One labelled section in a `FormSpec` (matches a Django admin fieldset).""" label: str description: str fields: tuple[FieldSpec, ...]
[docs] @dataclass(frozen=True, slots=True) class FormSpec: """Top-level spec for rendering a form with optional fieldsets.""" sections: tuple[FormSectionSpec, ...] non_field_errors: tuple[str, ...]
[docs] def field_spec(bound: BoundField, *, is_extra: bool = False) -> FieldSpec: """Classify a `BoundField` into a `FieldSpec`.""" widget = bound.field.widget if isinstance(widget, RelatedFieldWidgetWrapper): widget = widget.widget if isinstance(widget, django_forms.Textarea): kind: FieldKind = "textarea" input_type = "" elif isinstance(widget, django_forms.CheckboxInput): kind, input_type = "checkbox", "" elif isinstance(widget, django_forms.SelectMultiple): kind, input_type = "select_multi", "" elif isinstance(widget, django_forms.Select): kind, input_type = "select", "" else: kind = "input" input_type = getattr(widget, "input_type", "text") value = bound.value() selected: tuple[str, ...] = () if kind == "select_multi": raw = value if value is not None else [] if not isinstance(raw, (list, tuple)): raw = [raw] selected = tuple(str(v) for v in raw) return FieldSpec( bound=bound, kind=kind, input_type=input_type, value=value, selected=selected, is_extra=is_extra, )
[docs] def formset_spec(formset: BaseFormSet) -> FormsetSpec: """Build a `FormsetSpec` from a Django formset.""" rows: list[FormsetRowSpec] = [] for row_form in formset.forms: # Plain `BaseFormSet` rows do not expose `.instance`; treat absent # instance as "no pk", so `empty_permitted` alone marks the row blank. instance = getattr(row_form, "instance", None) is_extra = bool(row_form.empty_permitted and not getattr(instance, "pk", None)) visible_names = [ name for name in row_form.fields if name != "DELETE" and not row_form[name].is_hidden ] hidden_html = "".join( str(row_form[name]) for name in row_form.fields if row_form[name].is_hidden ) delete_field = row_form["DELETE"] if "DELETE" in row_form.fields else None rows.append( FormsetRowSpec( fields=tuple( field_spec(row_form[name], is_extra=is_extra) for name in visible_names ), hidden_html=hidden_html, delete_field=delete_field, errors={name: list(errs) for name, errs in row_form.errors.items()}, is_extra=is_extra, ) ) model = getattr(formset, "model", None) verbose_name_plural = ( str(model._meta.verbose_name_plural) if model is not None and hasattr(model, "_meta") else "" ) return FormsetSpec( prefix=formset.prefix or "", verbose_name_plural=verbose_name_plural, management_form=formset.management_form, rows=tuple(rows), non_form_errors=tuple(str(e) for e in formset.non_form_errors()), can_delete=bool(getattr(formset, "can_delete", False)), )
def _flatten_fieldset_names( fieldsets: Iterable[tuple[str | None, Mapping[str, Any]]], ) -> set[str]: """Flat set of field names referenced by `fieldsets` (nested tuples allowed).""" out: set[str] = set() for _, opts in fieldsets: for entry in opts.get("fields", ()): if isinstance(entry, (list, tuple)): out.update(entry) else: out.add(entry) return out
[docs] def form_spec( form: BaseForm, fieldsets: Sequence[tuple[str | None, Mapping[str, Any]]] | None = None, ) -> FormSpec: """Group `form`'s fields into sections per Django admin `(label, opts)`.""" sections: tuple[FormSectionSpec, ...] if fieldsets is None: all_fields = tuple(field_spec(form[name]) for name in form.fields) sections = (FormSectionSpec(label="", description="", fields=all_fields),) else: rendered: set[str] = set() built: list[FormSectionSpec] = [] for label, opts in fieldsets: specs: list[FieldSpec] = [] for entry in opts.get("fields", ()): names = entry if isinstance(entry, (list, tuple)) else (entry,) for name in names: if name in form.fields: specs.append(field_spec(form[name])) rendered.add(name) built.append( FormSectionSpec( label=label or "", description=str(opts.get("description") or ""), fields=tuple(specs), ) ) flat = _flatten_fieldset_names(fieldsets) leftover = tuple( field_spec(form[name]) for name in form.fields if name not in rendered and (not flat or name in flat) ) if leftover: built.append(FormSectionSpec(label="", description="", fields=leftover)) sections = tuple(built) non_field_errors = ( tuple(str(e) for e in form.non_field_errors()) if hasattr(form, "non_field_errors") else () ) return FormSpec(sections=sections, non_field_errors=non_field_errors)
__all__ = [ "FieldKind", "FieldSpec", "FormSectionSpec", "FormSpec", "FormsetRowSpec", "FormsetSpec", "field_spec", "form_spec", "formset_spec", ]