Skip to content

Commit

Permalink
Date related field can handle several formats
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Dec 23, 2021
1 parent 0beff0b commit 522c18a
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 32 deletions.
71 changes: 43 additions & 28 deletions src/wtforms/fields/datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@

class DateTimeField(Field):
"""
A text field which stores a `datetime.datetime` matching a format.
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.
"""

widget = widgets.DateTimeInput()
Expand All @@ -24,29 +27,33 @@ def __init__(
self, label=None, validators=None, format="%Y-%m-%d %H:%M:%S", **kwargs
):
super().__init__(label, validators, **kwargs)
self.format = format
self.strptime_format = clean_datetime_format_for_strptime(format)
self.format = format if isinstance(format, list) else [format]
self.strptime_format = clean_datetime_format_for_strptime(self.format)

def _value(self):
if self.raw_data:
return " ".join(self.raw_data)
return self.data and self.data.strftime(self.format) or ""
return self.data and self.data.strftime(self.format[0]) or ""

def process_formdata(self, valuelist):
if not valuelist:
return

date_str = " ".join(valuelist)
try:
self.data = datetime.datetime.strptime(date_str, self.strptime_format)
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid datetime value.")) from exc
for format in self.strptime_format:
try:
self.data = datetime.datetime.strptime(date_str, format)
return
except ValueError:
self.data = None

raise ValueError(self.gettext("Not a valid datetime value."))


class DateField(DateTimeField):
"""
Same as DateTimeField, except stores a `datetime.date`.
Same as :class:`~wtforms.fields.DateTimeField`, except stores a
:class:`datetime.date`.
"""

widget = widgets.DateInput()
Expand All @@ -59,18 +66,20 @@ def process_formdata(self, valuelist):
return

date_str = " ".join(valuelist)
try:
self.data = datetime.datetime.strptime(
date_str, self.strptime_format
).date()
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid date value.")) from exc
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.gettext("Not a valid date value."))


class TimeField(DateTimeField):
"""
Same as DateTimeField, except stores a `time`.
Same as :class:`~wtforms.fields.DateTimeField`, except stores a
:class:`datetime.time`.
"""

widget = widgets.TimeInput()
Expand All @@ -83,19 +92,20 @@ def process_formdata(self, valuelist):
return

time_str = " ".join(valuelist)
try:
self.data = datetime.datetime.strptime(
time_str, self.strptime_format
).time()
except ValueError as exc:
self.data = None
raise ValueError(self.gettext("Not a valid time value.")) from exc
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.gettext("Not a valid time value."))


class MonthField(DateField):
"""
Same as DateField, except represents a month, stores a `datetime.date`
with `day = 1`.
Same as :class:`~wtforms.fields.DateField`, except represents a month,
stores a :class:`datetime.date` with `day = 1`.
"""

widget = widgets.MonthInput()
Expand All @@ -106,7 +116,12 @@ def __init__(self, label=None, validators=None, format="%Y-%m", **kwargs):

class DateTimeLocalField(DateTimeField):
"""
Represents an ``<input type="datetime-local">``.
Same as :class:`~wtforms.fields.DateTimeField`, but represents an
``<input type="datetime-local">``.
"""

widget = widgets.DateTimeLocalInput()

def __init__(self, *args, **kwargs):
kwargs.setdefault("format", ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"])
super().__init__(*args, **kwargs)
13 changes: 9 additions & 4 deletions src/wtforms/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@
)


def clean_datetime_format_for_strptime(format):
def clean_datetime_format_for_strptime(formats):
"""
Remove dashes used to disable zero-padding with strftime formats (for
compatibiltity with strptime).
"""
return re.sub(
_DATETIME_STRIP_ZERO_PADDING_FORMATS_RE, lambda m: m[0].replace("-", ""), format
)
return [
re.sub(
_DATETIME_STRIP_ZERO_PADDING_FORMATS_RE,
lambda m: m[0].replace("-", ""),
format,
)
for format in formats
]


class UnsetValue:
Expand Down
11 changes: 11 additions & 0 deletions tests/fields/test_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ def test_microseconds():
F = make_form(a=DateTimeField(format="%Y-%m-%d %H:%M:%S.%f"))
form = F(DummyPostData(a=["2011-05-07 03:23:14.4242"]))
assert d == form.a.data


def test_multiple_formats():
d = datetime(2020, 3, 4, 5, 6)
F = make_form(a=DateTimeField(format=["%Y-%m-%d %H:%M", "%Y%m%d%H%M"]))

form = F(DummyPostData(a=["2020-03-04 05:06"]))
assert d == form.a.data

form = F(DummyPostData(a=["202003040506"]))
assert d == form.a.data
72 changes: 72 additions & 0 deletions tests/fields/test_datetimelocal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from datetime import datetime

from tests.common import DummyPostData

from wtforms.fields import DateTimeLocalField
from wtforms.form import Form


def make_form(name="F", **fields):
return type(str(name), (Form,), fields)


class F(Form):
a = DateTimeLocalField()
b = DateTimeLocalField(format="%Y-%m-%d %H:%M")
c = DateTimeLocalField(format="%-m/%-d/%Y %-I:%M")


def test_basic():
d = datetime(2008, 5, 5, 4, 30, 0, 0)
# Basic test with both inputs
form = F(
DummyPostData(
a=["2008-05-05", "04:30:00"], b=["2008-05-05 04:30"], c=["5/5/2008 4:30"]
)
)
assert form.a.data == d
assert (
form.a()
== '<input id="a" name="a" type="datetime-local" value="2008-05-05 04:30:00">'
)
assert form.b.data == d
assert (
form.b()
== '<input id="b" name="b" type="datetime-local" value="2008-05-05 04:30">'
)
assert form.c.data == d
assert (
form.c()
== '<input id="c" name="c" type="datetime-local" value="5/5/2008 4:30">'
)
assert form.validate()

# Test with a missing input
form = F(DummyPostData(a=["2008-05-05"]))
assert not form.validate()
assert form.a.errors[0] == "Not a valid datetime value."

form = F(a=d, b=d, c=d)
assert form.validate()
assert form.a._value() == "2008-05-05 04:30:00"
assert form.b._value() == "2008-05-05 04:30"
assert form.c._value() == "5/5/2008 4:30"


def test_microseconds():
d = datetime(2011, 5, 7, 3, 23, 14, 424200)
F = make_form(a=DateTimeLocalField(format="%Y-%m-%d %H:%M:%S.%f"))
form = F(DummyPostData(a=["2011-05-07 03:23:14.4242"]))
assert d == form.a.data


def test_separators():
dt = datetime(2008, 5, 5, 4, 30, 0, 0)

form = F(DummyPostData(a=["2008-05-05 04:30:00"]))
assert form.a.data == dt
assert form.validate()

form = F(DummyPostData(a=["2008-05-05T04:30:00"]))
assert form.a.data == dt
assert form.validate()

0 comments on commit 522c18a

Please sign in to comment.