Source code for next.forms.manager

"""`FormActionManager` aggregates backends and exposes URL patterns.

Backends are loaded lazily from `NEXT_FRAMEWORK["DEFAULT_FORM_ACTION_BACKENDS"]`
on first access. Unlike the components and static managers, this manager
does **not** subscribe to `settings_reloaded`, because `@action` registers
handlers imperatively at import time. Auto-rebuilding on every reload
would drop those registrations and break test runs that rely on
session-scoped `eager_load_pages`. Tests that swap form-action settings
must call `next.testing.reset_form_actions()` explicitly to drop the
cached backend list.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING, Any, cast

from next.conf import next_framework_settings

from .backends import FormActionFactory


if TYPE_CHECKING:
    from collections.abc import Callable, Iterator
    from pathlib import Path

    from django import forms as django_forms
    from django.http import HttpRequest
    from django.urls import URLPattern

    from .backends import FormActionBackend, FormActionOptions


logger = logging.getLogger(__name__)


[docs] class FormActionManager: """Holds one or more backends and yields their URL patterns."""
[docs] def __init__( self, backends: list[FormActionBackend] | None = None, ) -> None: """Initialise with explicit backends or defer loading to settings.""" self._backends: list[FormActionBackend] = list(backends) if backends else []
[docs] def __repr__(self) -> str: """Return a debug representation showing the number of backends.""" return f"<{self.__class__.__name__} backends={len(self._backends)}>"
[docs] def __iter__(self) -> Iterator[URLPattern]: """Yield concatenated URL patterns from each backend.""" self._ensure_backends() for backend in self._backends: yield from backend.generate_urls()
def _reload_config(self) -> None: self._backends = [] configs = cast( "list[Any]", getattr(next_framework_settings, "DEFAULT_FORM_ACTION_BACKENDS", []), ) for config in configs: if not isinstance(config, dict): continue try: self._backends.append(FormActionFactory.create_backend(config)) except Exception: logger.exception( "Error creating form-action backend from config %s", config, ) def _ensure_backends(self) -> None: if not self._backends: self._reload_config()
[docs] def register_action( self, name: str, handler: Callable[..., Any], *, options: FormActionOptions | None = None, ) -> None: """Forward registration to the first backend.""" self._ensure_backends() self._backends[0].register_action(name, handler, options=options)
[docs] def clear_registries(self) -> None: """Clear every backend that exposes a `clear_registry` method. Intended for test isolation. Backends that do not implement `clear_registry` are skipped silently. """ for backend in self._backends: clear = getattr(backend, "clear_registry", None) if callable(clear): clear()
[docs] def get_action_url(self, action_name: str) -> str: """Return the reverse URL from the first backend that knows `action_name`.""" self._ensure_backends() for backend in self._backends: if backend.get_meta(action_name) is not None: return backend.get_action_url(action_name) msg = f"Unknown form action: {action_name}" raise KeyError(msg)
[docs] def render_form_fragment( self, request: HttpRequest, action_name: str, form: django_forms.Form | None, template_fragment: str | None = None, *, page_file_path: Path | None = None, ) -> str: """Delegate rendering to the first backend.""" self._ensure_backends() return self._backends[0].render_form_fragment( request, action_name, form, template_fragment, page_file_path=page_file_path, )
@property def default_backend(self) -> FormActionBackend: """Return the first configured backend.""" self._ensure_backends() return self._backends[0]
form_action_manager = FormActionManager() __all__ = ["FormActionManager", "form_action_manager"]