Skip to content
4 changes: 4 additions & 0 deletions flask_admin/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ class T_FIELD_ARGS_VALIDATORS_ALLOW_BLANK(T_FIELD_ARGS_VALIDATORS):
allow_blank: NotRequired[bool]


class T_FIELD_ARGS_VALIDATORS_SELECTABLE(T_FIELD_ARGS_VALIDATORS_ALLOW_BLANK):
coerce: NotRequired[t.Callable[[t.Any], t.Any]]
Comment thread
samialfattani marked this conversation as resolved.


class T_FIELD_ARGS_VALIDATORS_FILES(T_FIELD_ARGS_VALIDATORS):
base_path: NotRequired[str]
allow_overwrite: NotRequired[bool]
Expand Down
6 changes: 5 additions & 1 deletion flask_admin/contrib/peewee/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from flask_admin.model.form import InlineFormAdmin

from ..._types import T_FIELD_ARGS_VALIDATORS_FILES
from ..._types import T_FIELD_ARGS_VALIDATORS_SELECTABLE
from ..._types import T_FILTER
from ..._types import T_PEEWEE_MODEL
from ..._types import T_WIDGET
Expand Down Expand Up @@ -338,7 +339,10 @@ def scaffold_form(self) -> type[Form]:
def scaffold_list_form(
self,
widget: type[T_WIDGET] | None = None,
validators: dict[str, T_FIELD_ARGS_VALIDATORS_FILES] | None = None,
validators: dict[
str, T_FIELD_ARGS_VALIDATORS_FILES | T_FIELD_ARGS_VALIDATORS_SELECTABLE
]
| None = None,
) -> type[Form]:
"""
Create form for the `index_view` using only the columns from
Expand Down
28 changes: 26 additions & 2 deletions flask_admin/contrib/sqla/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from sqlalchemy import Boolean
from sqlalchemy import Column
from sqlalchemy.orm import ColumnProperty
from sqlalchemy.sql.type_api import TypeEngine
from sqlalchemy_utils import ChoiceType
from wtforms import fields
from wtforms import Form
from wtforms import HiddenField
Expand Down Expand Up @@ -36,6 +38,7 @@
from ..._types import T_FIELD_ARGS_VALIDATORS
from ..._types import T_FIELD_ARGS_VALIDATORS_ALLOW_BLANK
from ..._types import T_FIELD_ARGS_VALIDATORS_FILES
from ..._types import T_FIELD_ARGS_VALIDATORS_SELECTABLE
from ..._types import T_INSTRUMENTED_ATTRIBUTE
from ..._types import T_MODEL_VIEW
from ..._types import T_ORM_MODEL
Expand Down Expand Up @@ -230,7 +233,7 @@ def convert(
if isinstance(prop, FieldPlaceholder):
return form.recreate_field(prop.field)

kwargs: T_FIELD_ARGS_VALIDATORS_ALLOW_BLANK = {"validators": [], "filters": []}
kwargs: T_FIELD_ARGS_VALIDATORS_SELECTABLE = {"validators": [], "filters": []}

if field_args:
kwargs.update(field_args) # type: ignore[typeddict-item]
Expand Down Expand Up @@ -360,7 +363,11 @@ def convert(
form_choices = getattr(self.view, "form_choices", None)
if mapper.class_ == self.view.model and form_choices:
choices = form_choices.get(prop.key)

if choices:
if "coerce" not in kwargs:
kwargs["coerce"] = coerce_factory(column.type)

return form.Select2Field( # type: ignore[misc]
choices=choices,
allow_blank=column.nullable, # type: ignore[arg-type]
Expand Down Expand Up @@ -655,6 +662,18 @@ def avoid_empty_strings(value: T) -> T | None:
return value if value else None


def coerce_factory(type_: TypeEngine[t.Any]) -> t.Callable[[t.Any], t.Any]:
Comment thread
samialfattani marked this conversation as resolved.
"""
Return a function to coerce a column, for use by Select2Field.
:param type_: Column type
"""

if isinstance(type_, ChoiceType):
return choice_type_coerce_factory(type_)
else:
return type_.python_type


def choice_type_coerce_factory(type_: T_CHOICE_TYPE) -> t.Callable[[t.Any], t.Any]:
"""
Return a function to coerce a ChoiceType column, for use by Select2Field.
Expand All @@ -671,8 +690,10 @@ def choice_type_coerce_factory(type_: T_CHOICE_TYPE) -> t.Callable[[t.Any], t.An
def choice_coerce(value: t.Any) -> t.Any:
if value is None:
return None

if isinstance(value, choice_cls):
return getattr(value, key)

return type_.python_type(value)

return choice_coerce
Expand All @@ -699,7 +720,10 @@ def get_form(
base_class: type[form.BaseForm] = form.BaseForm,
only: t.Collection[str | T_INSTRUMENTED_ATTRIBUTE] | None = None,
exclude: t.Collection[str | T_INSTRUMENTED_ATTRIBUTE] | None = None,
field_args: dict[str, T_FIELD_ARGS_VALIDATORS_FILES] | None = None,
field_args: dict[
str, T_FIELD_ARGS_VALIDATORS_FILES | T_FIELD_ARGS_VALIDATORS_SELECTABLE
]
| None = None,
hidden_pk: bool = False,
ignore_hidden: bool = True,
extra_fields: dict[str | T_INSTRUMENTED_ATTRIBUTE, UnboundField[t.Any]]
Expand Down
6 changes: 5 additions & 1 deletion flask_admin/contrib/sqla/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from ..._types import T_COLUMN
from ..._types import T_COLUMN_LIST
from ..._types import T_FIELD_ARGS_VALIDATORS_FILES
from ..._types import T_FIELD_ARGS_VALIDATORS_SELECTABLE
from ..._types import T_FILTER
from ..._types import T_INSTRUMENTED_ATTRIBUTE
from ..._types import T_SQLALCHEMY_COLUMN
Expand Down Expand Up @@ -892,7 +893,10 @@ def scaffold_form(self) -> type[Form]:
def scaffold_list_form(
self,
widget: type[T_WIDGET] | None = None,
validators: dict[str, T_FIELD_ARGS_VALIDATORS_FILES] | None = None,
validators: dict[
str, T_FIELD_ARGS_VALIDATORS_FILES | T_FIELD_ARGS_VALIDATORS_SELECTABLE
]
| None = None,
) -> type[Form]:
"""
Create form for the `index_view` using only the columns from
Expand Down
26 changes: 26 additions & 0 deletions flask_admin/form/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import time
import typing as t
from enum import Enum
from inspect import isclass

from wtforms import fields
from wtforms.form import BaseForm
Expand Down Expand Up @@ -196,6 +197,9 @@ def process_formdata(self, valuelist: t.Sequence[str] | None) -> None:
self.data = None
else:
try:
if isclass(self.coerce) and issubclass(self.coerce, Enum):
self.coerce = self._enum_coerce_factory(self.coerce)

self.data = self.coerce(valuelist[0])
except ValueError as err:
raise ValueError(
Expand All @@ -208,6 +212,28 @@ def pre_validate(self, form: BaseForm) -> None:

super().pre_validate(form)

def _enum_coerce_factory(self, type_: type[Enum]) -> t.Callable[[t.Any], t.Any]:
"""
Return a function to coerce an Enum column, for use by Select2Field.
:param type_: Enum class
"""

def enum_coerce(value: t.Any) -> t.Any:
if value is None:
return None

if isinstance(value, type_):
return value

ename = getattr(value, "name", value)
ename = str(value).replace(type_.__name__ + ".", "")
try:
return type_[ename]
except KeyError:
return type_(value)

return enum_coerce


class Select2TagsField(fields.StringField):
"""`Select2Tags <http://ivaynberg.github.com/select2/#tags>`_ styled text field.
Expand Down
18 changes: 15 additions & 3 deletions flask_admin/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from .._types import T_COLUMN_LIST
from .._types import T_COLUMN_TYPE_FORMATTERS
from .._types import T_FIELD_ARGS_VALIDATORS_FILES
from .._types import T_FIELD_ARGS_VALIDATORS_SELECTABLE
from .._types import T_FILTER
from .._types import T_INSTRUMENTED_ATTRIBUTE
from .._types import T_ORM_MODEL
Expand Down Expand Up @@ -654,7 +655,10 @@ class MyModelView(BaseModelView):

"""

form_args: dict[str, T_FIELD_ARGS_VALIDATORS_FILES] | None = None
form_args: (
dict[str, T_FIELD_ARGS_VALIDATORS_FILES | T_FIELD_ARGS_VALIDATORS_SELECTABLE]
| None
) = None
"""
Dictionary of form field arguments. Refer to WTForms documentation for
list of possible options.
Expand Down Expand Up @@ -1389,7 +1393,10 @@ def scaffold_form(self) -> type[Form]:
def scaffold_list_form(
self,
widget: type[T_WIDGET] | None = None,
validators: dict[str, T_FIELD_ARGS_VALIDATORS_FILES] | None = None,
validators: dict[
str, T_FIELD_ARGS_VALIDATORS_FILES | T_FIELD_ARGS_VALIDATORS_SELECTABLE
]
| None = None,
) -> type[Form]:
"""
Create form for the `index_view` using only the columns from
Expand Down Expand Up @@ -1442,7 +1449,12 @@ class MyModelView(BaseModelView):
def get_list_form(self):
return self.scaffold_list_form(widget=CustomWidget)
"""
validators: dict[str, T_FIELD_ARGS_VALIDATORS_FILES] | None = None
validators: (
dict[
str, T_FIELD_ARGS_VALIDATORS_FILES | T_FIELD_ARGS_VALIDATORS_SELECTABLE
]
| None
) = None
if self.form_args:
# get only validators, other form_args can break FieldList wrapper
validators = dict(
Expand Down
Loading