Skip to content

Commit

Permalink
feat: implement a readonly and a disabled validator
Browse files Browse the repository at this point in the history
  • Loading branch information
azmeuk committed Jul 22, 2023
1 parent f8fca13 commit 6aa6fd9
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Unreleased
- Added shorter format to :class:`~fields.DateTimeLocalField`
defaults :pr:`761`
- Stop support for python 3.7 :pr:`794`
- Implement :class:`~validators.ReadOnly` and
:class:`~validators.Disabled `:pr:`788`

Version 3.0.1
-------------
Expand Down
3 changes: 3 additions & 0 deletions docs/validators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ Built-in validators

.. autoclass:: wtforms.validators.NoneOf

.. autoclass:: wtforms.validators.ReadOnly

.. autoclass:: wtforms.validators.Disabled

.. _custom-validators:

Expand Down
40 changes: 40 additions & 0 deletions src/wtforms/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
"UUID",
"ValidationError",
"StopValidation",
"readonly",
"ReadOnly",
"disabled",
"Disabled",
)


Expand Down Expand Up @@ -676,6 +680,40 @@ def __call__(self, hostname):
return True


class ReadOnly:
"""
Set a field readonly.
Validation fails if the form data is different than the
field object data, or if unset, from the field default data.
"""

def __init__(self):
self.field_flags = {"readonly": True}

def __call__(self, form, field):
if field.data != field.object_data:
raise ValidationError(field.gettext("This field cannot be edited"))


class Disabled:
"""
Set a field disabled.
Validation fails if the form data is different than the
field object data, or if unset, from the field default data.
"""

def __init__(self):
self.field_flags = {"disabled": True}

def __call__(self, form, field):
if field.data != field.object_data:
raise ValidationError(
field.gettext("This field is disabled and cannot have a value")
)


email = Email
equal_to = EqualTo
ip_address = IPAddress
Expand All @@ -689,3 +727,5 @@ def __call__(self, hostname):
url = URL
any_of = AnyOf
none_of = NoneOf
readonly = ReadOnly
disabled = Disabled
78 changes: 60 additions & 18 deletions src/wtforms/widgets/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class Input:
"""

html_params = staticmethod(html_params)
validation_attrs = ["required"]
validation_attrs = ["required", "disabled"]

def __init__(self, input_type=None):
if input_type is not None:
Expand All @@ -185,7 +185,14 @@ class TextInput(Input):
"""

input_type = "text"
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
validation_attrs = [
"required",
"disabled",
"readonly",
"maxlength",
"minlength",
"pattern",
]


class PasswordInput(Input):
Expand All @@ -198,7 +205,14 @@ class PasswordInput(Input):
"""

input_type = "password"
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
validation_attrs = [
"required",
"disabled",
"readonly",
"maxlength",
"minlength",
"pattern",
]

def __init__(self, hide_value=True):
self.hide_value = hide_value
Expand Down Expand Up @@ -259,7 +273,7 @@ class FileInput(Input):
"""

input_type = "file"
validation_attrs = ["required", "accept"]
validation_attrs = ["required", "disabled", "accept"]

def __init__(self, multiple=False):
super().__init__()
Expand Down Expand Up @@ -297,7 +311,7 @@ class TextArea:
`rows` and `cols` ought to be passed as keyword args when rendering.
"""

validation_attrs = ["required", "maxlength", "minlength"]
validation_attrs = ["required", "disabled", "readonly", "maxlength", "minlength"]

def __call__(self, field, **kwargs):
kwargs.setdefault("id", field.id)
Expand Down Expand Up @@ -327,7 +341,7 @@ class Select:
`choices` is a iterable of `(value, label, selected)` tuples.
"""

validation_attrs = ["required"]
validation_attrs = ["required", "disabled"]

def __init__(self, multiple=False):
self.multiple = multiple
Expand Down Expand Up @@ -385,7 +399,14 @@ class SearchInput(Input):
"""

input_type = "search"
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
validation_attrs = [
"required",
"disabled",
"readonly",
"maxlength",
"minlength",
"pattern",
]


class TelInput(Input):
Expand All @@ -394,7 +415,14 @@ class TelInput(Input):
"""

input_type = "tel"
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
validation_attrs = [
"required",
"disabled",
"readonly",
"maxlength",
"minlength",
"pattern",
]


class URLInput(Input):
Expand All @@ -403,7 +431,14 @@ class URLInput(Input):
"""

input_type = "url"
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
validation_attrs = [
"required",
"disabled",
"readonly",
"maxlength",
"minlength",
"pattern",
]


class EmailInput(Input):
Expand All @@ -412,7 +447,14 @@ class EmailInput(Input):
"""

input_type = "email"
validation_attrs = ["required", "maxlength", "minlength", "pattern"]
validation_attrs = [
"required",
"disabled",
"readonly",
"maxlength",
"minlength",
"pattern",
]


class DateTimeInput(Input):
Expand All @@ -421,7 +463,7 @@ class DateTimeInput(Input):
"""

input_type = "datetime"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]


class DateInput(Input):
Expand All @@ -430,7 +472,7 @@ class DateInput(Input):
"""

input_type = "date"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]


class MonthInput(Input):
Expand All @@ -439,7 +481,7 @@ class MonthInput(Input):
"""

input_type = "month"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]


class WeekInput(Input):
Expand All @@ -448,7 +490,7 @@ class WeekInput(Input):
"""

input_type = "week"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]


class TimeInput(Input):
Expand All @@ -457,7 +499,7 @@ class TimeInput(Input):
"""

input_type = "time"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]


class DateTimeLocalInput(Input):
Expand All @@ -466,7 +508,7 @@ class DateTimeLocalInput(Input):
"""

input_type = "datetime-local"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]


class NumberInput(Input):
Expand All @@ -475,7 +517,7 @@ class NumberInput(Input):
"""

input_type = "number"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "readonly", "max", "min", "step"]

def __init__(self, step=None, min=None, max=None):
self.step = step
Expand All @@ -498,7 +540,7 @@ class RangeInput(Input):
"""

input_type = "range"
validation_attrs = ["required", "max", "min", "step"]
validation_attrs = ["required", "disabled", "max", "min", "step"]

def __init__(self, step=None):
self.step = step
Expand Down
41 changes: 41 additions & 0 deletions tests/validators/test_disabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from tests.common import DummyPostData

from wtforms import Form
from wtforms import StringField
from wtforms.validators import disabled


def test_disabled():
class F(Form):
disabled = StringField(validators=[disabled()])

form = F(disabled="foobar")
assert "disabled" in form.disabled.flags
assert (
form.disabled()
== '<input disabled id="disabled" name="disabled" type="text" value="foobar">'
)
assert form.validate()

form = F(DummyPostData(disabled=["foobar"]), data={"disabled": "foobar"})
assert form.validate()

form = F(DummyPostData(disabled=["foobar"]), data={"disabled": "foobarbaz"})
assert not form.validate()

form = F(DummyPostData(disabled=["foobar"]))
assert not form.validate()


def test_disabled_with_default():
class F(Form):
disabled = StringField(validators=[disabled()], default="foobar")

form = F(DummyPostData(disabled=["foobar"]))
assert form.validate()

form = F(DummyPostData(disabled=["foobar"]), data={"disabled": "foobarbaz"})
assert not form.validate()

form = F(DummyPostData(disabled=["foobarbaz"]), data={"disabled": "foobarbaz"})
assert form.validate()
38 changes: 38 additions & 0 deletions tests/validators/test_readonly.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from tests.common import DummyPostData

from wtforms import Form
from wtforms import StringField
from wtforms.validators import readonly


def test_readonly():
class F(Form):
ro = StringField(validators=[readonly()])

form = F(ro="foobar")
assert "readonly" in form.ro.flags
assert form.ro() == '<input id="ro" name="ro" readonly type="text" value="foobar">'
assert form.validate()

form = F(DummyPostData(ro=["foobar"]), data={"ro": "foobar"})
assert form.validate()

form = F(DummyPostData(ro=["foobar"]), data={"ro": "foobarbaz"})
assert not form.validate()

form = F(DummyPostData(ro=["foobar"]))
assert not form.validate()


def test_readonly_with_default():
class F(Form):
ro = StringField(validators=[readonly()], default="foobar")

form = F(DummyPostData(ro=["foobar"]))
assert form.validate()

form = F(DummyPostData(ro=["foobar"]), data={"ro": "foobarbaz"})
assert not form.validate()

form = F(DummyPostData(ro=["foobarbaz"]), data={"ro": "foobarbaz"})
assert form.validate()

0 comments on commit 6aa6fd9

Please sign in to comment.