Source code for fastapi_mongo_admin.admin.fields.widgets
"""Widget type constants and per-field override configuration."""
from __future__ import annotations
import enum
import types
import typing
from dataclasses import dataclass, field
from typing import Any
TEXT = "text"
TEXTAREA = "textarea"
NUMBER = "number"
CHECKBOX = "checkbox"
SELECT = "select"
RELATED_SELECT = "related_select"
DATE = "date"
DATETIME = "datetime-local"
EMAIL = "email"
JSON_EDITOR = "json"
TAGS = "tags"
OBJECT_ID = "objectid"
HIDDEN = "hidden"
STEP_INTEGER = "1"
STEP_DECIMAL = "0.01"
def _unwrap_annotation(annotation: Any) -> Any:
"""Unwrap Optional and union annotations."""
origin = typing.get_origin(annotation)
if origin is typing.Union or origin is types.UnionType:
args = [arg for arg in typing.get_args(annotation) if arg is not type(None)]
if args:
return _unwrap_annotation(args[0])
return annotation
[docs]
def is_primitive_list(annotation: Any) -> bool:
"""Return whether an annotation is a list of primitive scalar values."""
inner = _unwrap_annotation(annotation)
origin = typing.get_origin(inner)
if origin not in (list, typing.List):
return False
args = typing.get_args(inner)
if not args:
return True
element = _unwrap_annotation(args[0])
if element is str or element in (int, float, bool):
return True
if isinstance(element, type) and issubclass(element, enum.Enum):
return True
if typing.get_origin(element) is typing.Literal:
return True
return False
[docs]
def widget_for_type(
field_type: str,
*,
has_choices: bool = False,
annotation: Any = None,
) -> str:
"""Map an inferred field type to an HTML widget name.
Args:
field_type: Inferred admin field type.
has_choices: Whether the field has discrete choices.
annotation: Original field annotation for list element detection.
Returns:
Widget name string.
"""
if has_choices:
return SELECT
if field_type == "list" and annotation is not None and is_primitive_list(annotation):
return TAGS
mapping = {
"str": TEXT,
"int": NUMBER,
"float": NUMBER,
"decimal": NUMBER,
"bool": CHECKBOX,
"datetime": DATETIME,
"date": DATE,
"list": JSON_EDITOR,
"dict": JSON_EDITOR,
"ObjectId": OBJECT_ID,
}
return mapping.get(field_type, TEXT)
[docs]
def step_for_type(field_type: str) -> str | None:
"""Return the HTML ``step`` attribute for numeric field types.
Args:
field_type: Inferred admin field type.
Returns:
Step string for numeric widgets, or ``None``.
"""
if field_type == "int":
return STEP_INTEGER
if field_type in ("float", "decimal"):
return STEP_DECIMAL
return None
[docs]
@dataclass
class FieldWidget:
"""Override default widget and HTML attributes for a model field."""
widget: str | None = None
"""Widget name override."""
attrs: dict[str, Any] = field(default_factory=dict)
"""Extra HTML attributes merged onto the rendered control."""
[docs]
@classmethod
def from_mapping(cls, data: FieldWidget | dict[str, Any]) -> FieldWidget:
"""Build from a FieldWidget instance or shorthand dict.
Args:
data: ``FieldWidget`` or dict with optional ``widget`` key plus
HTML attribute overrides.
Returns:
Normalized ``FieldWidget`` instance.
"""
if isinstance(data, FieldWidget):
return data
mapping = dict(data)
widget = mapping.pop("widget", None)
return cls(widget=widget, attrs=mapping)
[docs]
def apply_field_widget_override(
admin_field: Any,
override: FieldWidget | dict[str, Any],
) -> None:
"""Apply a widget override onto an ``AdminField``.
Args:
admin_field: ``AdminField`` instance to mutate.
override: ``FieldWidget`` or shorthand dict.
Returns:
None.
"""
config = FieldWidget.from_mapping(override)
if config.widget:
admin_field.widget = config.widget
admin_field.attrs.update(config.attrs)