import datetime
import warnings
from collections.abc import Callable
from wtforms import widgets
from wtforms.fields.core import Field
from wtforms.utils import clean_datetime_format_for_strptime
__all__ = (
"DateTimeField",
"DateField",
"TimeField",
"MonthField",
"DateTimeLocalField",
"WeekField",
)
[docs]class DateTimeField(Field):
"""
A text field which stores a :class:`datetime.datetime` matching one or
several formats. If ``format`` is a list, any input value matching any
format will be accepted, and the first format in the list will be used
to produce HTML values.
.. deprecated:: 3.2.3
``DateTimeField`` renders ``<input type="datetime">``, which is
obsolete. Use :class:`DateTimeLocalField` instead. ``DateTimeField``
will be removed in WTForms 3.4.
"""
widget = widgets.DateTimeInput()
def __init__(
self,
label=None,
validators=None,
format="%Y-%m-%d %H:%M:%S",
invalid_value_message=None,
**kwargs,
):
super().__init__(label, validators, **kwargs)
if type(self) is DateTimeField:
warnings.warn(
"'DateTimeField' renders <input type=\"datetime\">, which is"
" obsolete. Use 'DateTimeLocalField' instead. 'DateTimeField'"
" will be removed in WTForms 3.4.",
DeprecationWarning,
stacklevel=2,
)
self.format = format if isinstance(format, list) else [format]
self.strptime_format = clean_datetime_format_for_strptime(self.format)
self.invalid_value_message = invalid_value_message or self.gettext(
"Not a valid datetime value."
)
def _value(self):
if self.raw_data:
return " ".join(self.raw_data)
format = self.format[0]
return self.data and self.data.strftime(format) or ""
def process_formdata(self, valuelist):
if not valuelist:
return
date_str = " ".join(valuelist)
for format in self.strptime_format:
try:
self.data = datetime.datetime.strptime(date_str, format)
return
except ValueError:
self.data = None
raise ValueError(self.invalid_value_message)
[docs]class DateField(DateTimeField):
"""
Same as :class:`~wtforms.fields.DateTimeField`, except stores a
:class:`datetime.date` and renders as an :mdn-input:`date`.
"""
widget = widgets.DateInput()
def __init__(
self,
label=None,
validators=None,
format="%Y-%m-%d",
invalid_value_message=None,
**kwargs,
):
super().__init__(
label,
validators,
format,
invalid_value_message=invalid_value_message,
**kwargs,
)
self.invalid_value_message = invalid_value_message or self.gettext(
"Not a valid date value."
)
def process_formdata(self, valuelist):
if not valuelist:
return
date_str = " ".join(valuelist)
for format in self.strptime_format:
try:
self.data = datetime.datetime.strptime(date_str, format).date()
return
except ValueError:
self.data = None
raise ValueError(self.invalid_value_message)
[docs]class TimeField(DateTimeField):
"""
Same as :class:`~wtforms.fields.DateTimeField`, except stores a
:class:`datetime.time` and renders as an :mdn-input:`time`.
"""
widget = widgets.TimeInput()
def __init__(
self,
label=None,
validators=None,
format="%H:%M",
invalid_value_message=None,
**kwargs,
):
super().__init__(
label,
validators,
format,
invalid_value_message=invalid_value_message,
**kwargs,
)
self.invalid_value_message = invalid_value_message or self.gettext(
"Not a valid time value."
)
def process_formdata(self, valuelist):
if not valuelist:
return
time_str = " ".join(valuelist)
for format in self.strptime_format:
try:
self.data = datetime.datetime.strptime(time_str, format).time()
return
except ValueError:
self.data = None
raise ValueError(self.invalid_value_message)
[docs]class MonthField(DateField):
"""
Same as :class:`~wtforms.fields.DateField`, except represents a month,
stores a :class:`datetime.date` with `day = 1`, and renders as an
:mdn-input:`month`.
"""
widget = widgets.MonthInput()
def __init__(self, label=None, validators=None, format="%Y-%m", **kwargs):
super().__init__(label, validators, format, **kwargs)
class WeekField(DateField):
"""
Same as :class:`~wtforms.fields.DateField`, except represents a week,
stores a :class:`datetime.date` of the monday of the given week, and
renders as an :mdn-input:`week`.
"""
widget = widgets.WeekInput()
def __init__(
self,
label=None,
validators=None,
format="%Y-W%W",
invalid_value_message=None,
**kwargs,
):
super().__init__(
label,
validators,
format,
invalid_value_message=invalid_value_message,
**kwargs,
)
self.invalid_value_message = invalid_value_message or self.gettext(
"Not a valid week value."
)
def process_formdata(self, valuelist):
if not valuelist:
return
time_str = " ".join(valuelist)
for format in self.strptime_format:
try:
if "%w" not in format:
# The '%w' week starting day is needed. This defaults it to monday
# like ISO 8601 indicates.
self.data = datetime.datetime.strptime(
f"{time_str}-1", f"{format}-%w"
).date()
else:
self.data = datetime.datetime.strptime(time_str, format).date()
return
except ValueError:
self.data = None
raise ValueError(self.invalid_value_message)
[docs]class DateTimeLocalField(DateTimeField):
"""
Same as :class:`~wtforms.fields.DateTimeField`, but represents an
:mdn-input:`datetime-local`.
:param tz:
Optional timezone associated with the input. The HTML
``datetime-local`` widget always renders and submits a naive
local datetime; ``tz`` declares the zone in which that local
datetime should be interpreted. Accepts:
- ``None`` (default): legacy behavior, :attr:`data` is naive.
- a :class:`datetime.tzinfo` instance: parsed values get this
zone attached, and aware values rendered through the field
are converted to it before being formatted.
- a callable returning a :class:`datetime.tzinfo` (or ``None``):
resolved on each access, useful when the zone depends on the
request context (e.g. user preferences). Returning ``None``
falls back to the naive behavior.
No correction is applied for DST gaps or overlaps — submitted
values are annotated as-is via ``replace(tzinfo=...)``.
"""
widget = widgets.DateTimeLocalInput()
def __init__(
self,
*args,
tz: datetime.tzinfo | Callable[[], datetime.tzinfo | None] | None = None,
**kwargs,
):
kwargs.setdefault(
"format",
[
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%dT%H:%M",
],
)
super().__init__(*args, **kwargs)
self.tz = tz
def _resolve_tz(self):
return self.tz() if callable(self.tz) else self.tz
def _value(self):
"""Render :attr:`data`, converting aware values to ``tz`` and stripping
the zone before formatting."""
if self.raw_data:
return " ".join(self.raw_data)
if not self.data:
return ""
value = self.data
tz = self._resolve_tz()
if tz is not None and value.tzinfo is not None:
value = value.astimezone(tz).replace(tzinfo=None)
return value.strftime(self.format[0])
def process_formdata(self, valuelist):
"""Parse the submitted value and annotate it with ``tz`` if set."""
super().process_formdata(valuelist)
tz = self._resolve_tz()
if tz is not None and self.data is not None:
self.data = self.data.replace(tzinfo=tz)