8000 implement DateRange validator similar to NumberRange by jkittner · Pull Request #787 · pallets-eco/wtforms · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

implement DateRange validator similar to NumberRange #787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/validators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ Built-in validators

.. autoclass:: wtforms.validators.NumberRange

.. autoclass:: wtforms.validators.DateRange

This validator can be used with a custom callback to make it somewhat dynamic::

from datetime import date
from datetime import datetime
from datetime import timedelta
from functools import partial

from wtforms import Form
from wtforms.fields import DateField
from wtforms.fields import DateTimeLocalField
from wtforms.validators import DateRange


def in_n_days(days):
return datetime.now() + timedelta(days=days)


cb = partial(in_n_days, 5)


class DateForm(Form):
date = DateField("date", [DateRange(min=date(2023, 1, 1), max_callback=cb)])
datetime = DateTimeLocalField(
"datetime-local",
[
DateRange(
min=datetime(2023, 1, 1, 15, 30),
max_callback=cb,
input_type="datetime-local",
)
],
)

In the example, we use the DateRange validator to prevent a date outside of a
specified range. for the field ``date`` we set the minimum range statically,
but the date must not be newer than the current time + 5 days. For the field
``datetime`` we do the same, but specify an input_type to achieve the correct
formatting for the corresponding field type.


.. autoclass:: wtforms.validators.Optional

This also sets the ``optional`` :attr:`flag <wtforms.fields.Field.flags>` on
Expand Down
108 changes: 108 additions & 0 deletions src/wtforms/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import math
import re
import uuid
from datetime import date
from datetime import datetime

__all__ = (
"DataRequired",
Expand All @@ -17,6 +19,7 @@
"Length",
"length",
"NumberRange",
"DateRange",
"number_range",
"Optional",
"optional",
Expand Down Expand Up @@ -224,6 +227,110 @@ def __call__(self, form, field):
raise ValidationError(message % dict(min=self.min, max=self.max))


class DateRange:
"""
Validates that a date or datetime is of a minimum and/or maximum value,
inclusive. This will work with dates and datetimes.

:param min:
The minimum required date or datetime. If not provided, minimum
date or datetime will not be checked.
:param max:
The maximum date or datetime. If not provided, maximum date or datetime
will not be checked.
:param message:
Error message to raise in case of a validation error. Can be
interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults
are provided depending on the existence of min and max.
:param input_type:
The type of field to check. Either ``datetime-local`` or ``date``. If
``datetime-local`` the attributes (``min``, ``max``) are set using the
``YYYY-MM-DDThh:mm`` format, if ``date`` (the default), ``yyyy-mm-dd``
is used.
:param min_callback:
dynamically set the minimum date or datetime based on the return value of
a function. The specified function must not take any arguments.
:param max_callback:
dynamically set the maximum date or datetime based on the return value of
a function. The specified function must not take any arguments.

When supported, sets the `min` and `max` attributes on widgets.
"""

def __init__(
self,
min=None,
max=None,
message=None,
input_type="date",
min_callback=None,
max_callback=None,
):
if min and min_callback:
raise ValueError("You can only specify one of min or min_callback.")

if max and max_callback:
raise ValueError("You can only specify one of max or max_callback.")

if input_type not in ("datetime-local", "date"):
raise ValueError(
f"Only datetime-local or date are allowed, not {input_type!r}"
)

self.min = min
self.max = max
self.message = message
self.min_callback = min_callback
self.max_callback = max_callback
self.field_flags = {}
if input_type == "date":
fmt = "%Y-%m-%d"
else:
fmt = "%Y-%m-%dT%H:%M"

if self.min is not None:
self.field_flags["min"] = self.min.strftime(fmt)
if self.max is not None:
self.field_flags["max"] = self.max.strftime(fmt)

def __call__(self, form, field):
if self.min_callback is not None:
self.min = self.min_callback()

if self.max_callback is not None:
self.max = self.max_callback()

if isinstance(self.min, date):
self.min = datetime(*self.min.timetuple()[:5])

if isinstance(self.max, date):
self.max = datetime(*self.max.timetuple()[:5])

data = field.data
if data is not None:
if isinstance(data, date):
data = datetime(*data.timetuple()[:5])

if (self.min is None or data >= self.min) and (
self.max is None or data <= self.max
):
return

if self.message is not None:
message = self.message

elif self.max is None:
message = field.gettext("Date must be at least %(min)s.")

elif self.min is None:
message = field.gettext("Date must be at most %(max)s.")

else:
message = field.gettext("Date must be between %(min)s and %(max)s.")

raise ValidationError(message % dict(min=self.min, max=self.max))


class Optional:
"""
Allows empty input and stops the validation chain from continuing.
Expand Down Expand Up @@ -723,6 +830,7 @@ def __call__(self, form, field):
mac_address = MacAddress
length = Length
number_range = NumberRange
date_range = DateRange
optional = Optional
input_required = InputRequired
data_required = DataRequired
Expand Down
151 changes: 151 additions & 0 deletions tests/validators/test_date_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from datetime import date
from datetime import datetime

import pytest

from wtforms.validators import DateRange
from wtforms.validators import ValidationError


@pytest.mark.parametrize(
("min_v", "max_v", "test_v"),
(
(datetime(2023, 5, 23, 18), datetime(2023, 5, 25), date(2023, 5, 24)),
(date(2023, 5, 24), datetime(2023, 5, 25), datetime(2023, 5, 24, 15)),
(datetime(2023, 5, 24), None, date(2023, 5, 25)),
(None, datetime(2023, 5, 25), datetime(2023, 5, 24)),
),
)
def test_date_range_passes(min_v, max_v, test_v, dummy_form, dummy_field):
"""
It should pass if the test_v is between min_v and max_v
"""
dummy_field.data = test_v
validator = DateRange(min_v, max_v)
validator(dummy_form, dummy_field)


@pytest.mark.parametrize(
("min_v", "max_v", "test_v"),
(
(date(2023, 5, 24), date(2023, 5, 25), None),
(datetime(2023, 5, 24, 18, 3), date(2023, 5, 25), None),
(datetime(2023, 5, 24), datetime(2023, 5, 25), None),
(datetime(2023, 5, 24), datetime(2023, 5, 25), datetime(2023, 5, 20)),
(datetime(2023, 5, 24), datetime(2023, 5, 25), datetime(2023, 5, 26)),
(datetime(2023, 5, 24), None, datetime(2023, 5, 23)),
(None, datetime(2023, 5, 25), datetime(2023, 5, 26)),
),
)
def test_date_range_raises(min_v, max_v, test_v, dummy_form, dummy_field):
"""
It should raise ValidationError if the test_v is not between min_v and max_v
"""
dummy_field.data = test_v
validator = DateRange(min_v, max_v)
with pytest.raises(ValidationError):
validator(dummy_form, dummy_field)


@pytest.mark.parametrize(
("min_v", "max_v", "min_flag", "max_flag"),
(
(datetime(2023, 5, 24), datetime(2023, 5, 25), "2023-05-24", "2023-05-25"),
(None, datetime(2023, 5, 25), None, "2023-05-25"),
(datetime(2023, 5, 24), None, "2023-05-24", None),
),
)
def test_date_range_field_flags_are_set_date(min_v, max_v, min_flag, max_flag):
"""
It should format the min and max attribute as yyyy-mm-dd
when input_type is ``date`` (default)
"""
validator = DateRange(min_v, max_v)
assert validator.field_flags.get("min") == min_flag
assert validator.field_flags.get("max") == max_flag


@pytest.mark.parametrize(
("min_v", "max_v", "min_flag", "max_flag"),
(
(date(2023, 5, 24), date(2023, 5, 25), "2023-05-24T00:00", "2023-05-25T00:00"),
(None, date(2023, 5, 25), None, "2023-05-25T00:00"),
(date(2023, 5, 24), None, "2023-05-24T00:00", None),
),
)
def test_date_range_field_flags_are_set_datetime(min_v, max_v, min_flag, max_flag):
"""
It should format the min and max attribute as YYYY-MM-DDThh:mm
when input_type is ``datetime-local`` (default)
"""
validator = DateRange(min_v, max_v, input_type="datetime-local")
assert validator.field_flags.get("min") == min_flag
assert validator.field_flags.get("max") == max_flag


def test_date_range_input_type_invalid():
"""
It should raise if the input_type is not either datetime-local or date
"""
with pytest.raises(ValueError) as exc_info:
DateRange(input_type="foo")

(err_msg,) = exc_info.value.args
assert err_msg == "Only datetime-local or date are allowed, not 'foo'"


def _dt_callback_min():
return datetime(2023, 5, 24, 15, 3)


def _d_callback_min():
return date(2023, 5, 24)


def _dt_callback_max():
return datetime(2023, 5, 25, 0, 3)


def _d_callback_max():
return date(2023, 5, 25)


@pytest.mark.parametrize(
("min_v", "max_v", "test_v"),
(
(_dt_callback_min, _dt_callback_max, datetime(2023, 5, 24, 15, 4)),
(_d_callback_min, _d_callback_max, datetime(2023, 5, 24, 15, 4)),
(_dt_callback_min, None, datetime(2023, 5, 24, 15, 4)),
(None, _dt_callback_max, datetime(2023, 5, 24, 15, 2)),
(None, _dt_callback_max, date(2023, 5, 24)),
),
)
def test_date_range_passes_with_callback(min_v, max_v, test_v, dummy_form, dummy_field):
"""
It should pass with a callback set as either min or max
"""
dummy_field.data = test_v
validator = DateRange(min_callback=min_v, max_callback=max_v)
validator(dummy_form, dummy_field)


def test_date_range_min_callback_and_value_set():
"""
It should raise if both, a value and a callback are set for min
"""
with pytest.raises(ValueError) as exc_info:
DateRange(min=date(2023, 5, 24), min_callback=_dt_callback_min)

(err_msg,) = exc_info.value.args
assert err_msg == "You can only specify one of min or min_callback."


def test_date_range_max_callback_and_value_set():
"""
It should raise if both, a value and a callback are set for max
"""
with pytest.raises(ValueError) as exc_info:
DateRange(max=date(2023, 5, 24), max_callback=_dt_callback_max)

(err_msg,) = exc_info.value.args
assert err_msg == "You can only specify one of max or max_callback."
Loading
0