Source code for flask_admin.form.fields
import datetime
import json
import re
import time
import typing as t
from enum import Enum
from wtforms import fields
from wtforms.form import BaseForm
from flask_admin._compat import _iter_choices_wtforms_compat
from flask_admin._compat import as_unicode
from flask_admin._compat import text_type
from flask_admin.babel import gettext
from .._types import T_ITER_CHOICES
from .._types import T_TRANSLATABLE
from .._types import T_VALIDATOR
from . import widgets as admin_widgets
"""
An understanding of WTForms's Custom Widgets is helpful for understanding this code:
https://wtforms.readthedocs.io/widgets/#custom-widgets
"""
__all__ = [
"DateTimeField",
"TimeField",
"Select2Field",
"Select2TagsField",
"JSONField",
]
class DateTimeField(fields.DateTimeField):
"""
Allows modifying the datetime format of a DateTimeField using form_args.
"""
widget = admin_widgets.DateTimePickerWidget()
def __init__(
self,
label: T_TRANSLATABLE | None = None,
validators: list[T_VALIDATOR] | None = None,
format: str | None = None,
**kwargs: t.Any,
) -> None:
"""
Constructor
:param label:
Label
:param validators:
Field validators
:param format:
Format for text to date conversion. Defaults to '%Y-%m-%d %H:%M:%S'
:param kwargs:
Any additional parameters
"""
super().__init__(label, validators, format or "%Y-%m-%d %H:%M:%S", **kwargs)
[docs]
class TimeField(fields.Field):
"""
A text field which stores a `datetime.time` object.
Accepts time string in multiple formats: 20:10, 20:10:00, 10:00 am, 9:30pm, etc.
"""
widget = admin_widgets.TimePickerWidget()
def __init__(
self,
label: T_TRANSLATABLE | None = None,
validators: list[T_VALIDATOR] | None = None,
formats: t.Iterable[str] | None = None,
default_format: str | None = None,
widget_format: t.Any = None,
**kwargs: t.Any,
) -> None:
"""
Constructor
:param label:
Label
:param validators:
Field validators
:param formats:
Supported time formats, as a enumerable.
:param default_format:
Default time format. Defaults to '%H:%M:%S'
:param kwargs:
Any additional parameters
"""
super().__init__(label, validators, **kwargs)
self.formats = formats or (
"%H:%M:%S",
"%H:%M",
"%I:%M:%S%p",
"%I:%M%p",
"%I:%M:%S %p",
"%I:%M %p",
)
self.default_format = default_format or "%H:%M:%S"
def _value(self) -> str:
if self.raw_data:
return " ".join(self.raw_data)
elif self.data is not None:
return self.data.strftime(self.default_format)
else:
return ""
[docs]
def process_formdata(self, valuelist: t.Iterable[str]) -> None:
if valuelist:
date_str = " ".join(valuelist)
if date_str.strip():
for format in self.formats:
try:
timetuple = time.strptime(date_str, format)
self.data = datetime.time(
timetuple.tm_hour, timetuple.tm_min, timetuple.tm_sec
)
return
except ValueError:
pass
raise ValueError(gettext("Invalid time format"))
else:
self.data = None
[docs]
class Select2Field(fields.SelectField):
"""
`Select2 <https://github.com/ivaynberg/select2>`_ styled select widget.
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to
work.
"""
widget = admin_widgets.Select2Widget()
def __init__(
self,
label: T_TRANSLATABLE | None = None,
validators: list[T_VALIDATOR] | None = None,
coerce: t.Callable[[t.Any], t.Any] = text_type,
choices: tuple[str, ...] | Enum | None = None,
allow_blank: bool = False,
blank_text: T_TRANSLATABLE | None = None,
**kwargs: t.Any,
) -> None:
super().__init__(
label,
validators,
coerce,
choices, # type: ignore[arg-type]
**kwargs,
)
self.allow_blank = allow_blank
self.blank_text = blank_text or " "
[docs]
def iter_choices(self) -> t.Iterator[T_ITER_CHOICES]: # type: ignore[override]
if self.allow_blank:
yield _iter_choices_wtforms_compat(
"__None", self.blank_text, self.data is None
)
for choice in self.choices or ():
if isinstance(choice, tuple):
yield _iter_choices_wtforms_compat(
choice[0], choice[1], self.coerce(choice[0]) == self.data
)
else:
yield _iter_choices_wtforms_compat(
choice.value, # type: ignore[attr-defined]
choice.name, # type: ignore[attr-defined]
self.coerce(choice.value) == self.data, # type: ignore[attr-defined]
)
[docs]
def process_data(self, value: t.Any) -> None:
if value is None:
self.data = None
else:
try:
self.data = self.coerce(value)
except (ValueError, TypeError):
self.data = None
[docs]
def process_formdata(self, valuelist: t.Sequence[str] | None) -> None:
if valuelist:
if valuelist[0] == "__None":
self.data = None
else:
try:
self.data = self.coerce(valuelist[0])
except ValueError as err:
raise ValueError(
self.gettext("Invalid Choice: could not coerce")
) from err
[docs]
def pre_validate(self, form: BaseForm) -> None:
if self.allow_blank and self.data is None:
return
super().pre_validate(form)
[docs]
class Select2TagsField(fields.StringField):
"""`Select2Tags <http://ivaynberg.github.com/select2/#tags>`_ styled text field.
You must include select2.js, form-x.x.x.js and select2 stylesheet for it to work.
"""
widget = admin_widgets.Select2TagsWidget()
_strip_regex = re.compile(
r"#\d+(?:(,)|\s$)"
) # e.g., 'tag#123, anothertag#425 ' => 'tag, anothertag'
def __init__(
self,
label: T_TRANSLATABLE | None = None,
validators: list[T_VALIDATOR] | None = None,
save_as_list: bool = False,
coerce: t.Callable[[t.Any], t.Any] = text_type,
allow_duplicates: bool = False,
**kwargs: t.Any,
) -> None:
"""Initialization
:param save_as_list:
If `True` then populate ``obj`` using list else string
:param allow_duplicates:
If `True` then duplicate tags are allowed in the field.
"""
self.save_as_list = save_as_list
self.allow_duplicates = allow_duplicates
self.coerce = coerce
super().__init__(label, validators, **kwargs)
[docs]
def process_formdata(self, valuelist: t.Sequence[str] | None = None) -> None:
if valuelist:
entrylist = valuelist[0]
if self.allow_duplicates and entrylist.endswith(" "):
# This means this is an allowed duplicate (see form.js,
# `createSearchChoice`), so its ID was modified. Hence, we need to
# restore the original IDs.
entrylist = re.sub(self._strip_regex, "\\1", entrylist)
if self.save_as_list:
self.data = [ # type: ignore[assignment]
self.coerce(v.strip()) for v in entrylist.split(",") if v.strip()
]
else:
self.data = self.coerce(entrylist)
def _value(self) -> str:
if isinstance(self.data, list | tuple):
return ",".join(as_unicode(v) for v in self.data)
elif self.data:
return as_unicode(self.data)
else:
return ""
class JSONField(fields.TextAreaField):
def _value(self) -> str:
if self.raw_data:
return self.raw_data[0]
elif self.data:
# prevent utf8 characters from being converted to ascii
return as_unicode(json.dumps(self.data, ensure_ascii=False))
else:
return "{}"
def process_formdata(self, valuelist: t.Sequence[str] | None) -> None:
if valuelist:
value = valuelist[0]
# allow saving blank field as None
if not value:
self.data = None
return
try:
self.data = json.loads(valuelist[0])
except ValueError as err:
raise ValueError(self.gettext("Invalid JSON")) from err