Skip to content
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

SelectField choice refactoring #739

Merged
merged 1 commit into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
- Translation improvements :pr:`732` :pr:`734` :pr:`754`
- Implement :class:`~fields.ColorField` :pr:`755`
- Delayed import of ``email_validator``. :issue:`727`
- ``<option>`` attributes can be passed by the :class:`~fields.SelectField`
``choices`` parameter :issue:`692` :pr:`738`
- Use the standard datetime formats by default for
:class:`~fields.DateTimeLocalField` :pr:`761`
- Python 3.11 support :pr:`763`
Expand Down
6 changes: 4 additions & 2 deletions docs/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,10 @@ refer to a single input from the form.

Select fields take a ``choices`` parameter which is either:

* a list of ``(value, label)`` pairs. It can also be a list of only values, in
which case the value is used as the label. The value can be of any
* a list of ``(value, label)`` or ``(value, label, render_kw)`` tuples.
It can also be a list of only values, in which case the value is used
as the label. If set, the ``render_kw`` dictionnary will be rendered as
HTML ``<option>`` parameters. The value can be of any
type, but because form data is sent to the browser as strings, you
will need to provide a ``coerce`` function that converts a string
back to the expected type.
Expand Down
18 changes: 11 additions & 7 deletions src/wtforms/fields/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ def __iter__(self):
_form=None,
_meta=self.meta,
)
for i, (value, label, checked) in enumerate(self.iter_choices()):
opt = self._Option(label=label, id="%s-%d" % (self.id, i), **opts)
for i, (value, label, checked, render_kw) in enumerate(self.iter_choices()):
opt = self._Option(
label=label, id="%s-%d" % (self.id, i), **opts, **render_kw
)
opt.process(None, value)
opt.checked = checked
yield opt
Expand Down Expand Up @@ -112,8 +114,9 @@ def _choices_generator(self, choices):
else:
_choices = zip(choices, choices)

for value, label in _choices:
yield (value, label, self.coerce(value) == self.data)
for value, label, *other_args in _choices:
render_kw = other_args[0] if len(other_args) else {}
yield (value, label, self.coerce(value) == self.data, render_kw)

def process_data(self, value):
try:
Expand All @@ -138,7 +141,7 @@ def pre_validate(self, form):
if not self.validate_choice:
return

for _, _, match in self.iter_choices():
for _, _, match, _ in self.iter_choices():
if match:
break
else:
Expand All @@ -163,9 +166,10 @@ def _choices_generator(self, choices):
else:
_choices = []

for value, label in _choices:
for value, label, *args in _choices:
selected = self.data is not None and self.coerce(value) in self.data
yield (value, label, selected)
render_kw = args[0] if len(args) else {}
yield (value, label, selected, render_kw)

def process_data(self, value):
try:
Expand Down
8 changes: 4 additions & 4 deletions src/wtforms/widgets/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,12 +358,12 @@ def __call__(self, field, **kwargs):
if field.has_groups():
for group, choices in field.iter_groups():
html.append("<optgroup %s>" % html_params(label=group))
for val, label, selected in choices:
html.append(self.render_option(val, label, selected))
for val, label, selected, render_kw in choices:
html.append(self.render_option(val, label, selected, **render_kw))
html.append("</optgroup>")
else:
for val, label, selected in field.iter_choices():
html.append(self.render_option(val, label, selected))
for val, label, selected, render_kw in field.iter_choices():
html.append(self.render_option(val, label, selected, **render_kw))
html.append("</select>")
return Markup("".join(html))

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def basic_widget_dummy_field(dummy_field_class):

@pytest.fixture
def select_dummy_field(dummy_field_class):
return dummy_field_class([("foo", "lfoo", True), ("bar", "lbar", False)])
return dummy_field_class([("foo", "lfoo", True, {}), ("bar", "lbar", False, {})])


@pytest.fixture
Expand Down
40 changes: 38 additions & 2 deletions tests/fields/test_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def test_optgroup():
'<option selected value="a">Foo</option>'
"</optgroup>" in form.a()
)
assert list(form.a.iter_choices()) == [("a", "Foo", True)]
assert list(form.a.iter_choices()) == [("a", "Foo", True, {})]


def test_optgroup_shortcut():
Expand All @@ -232,7 +232,10 @@ def test_optgroup_shortcut():
'<option selected value="bar">bar</option>'
"</optgroup>" in form.a()
)
assert list(form.a.iter_choices()) == [("foo", "foo", False), ("bar", "bar", True)]
assert list(form.a.iter_choices()) == [
("foo", "foo", False, {}),
("bar", "bar", True, {}),
]


@pytest.mark.parametrize("choices", [[], ()])
Expand All @@ -241,3 +244,36 @@ def test_empty_optgroup(choices):
form = F(a="bar")
assert '<optgroup label="hello"></optgroup>' in form.a()
assert list(form.a.iter_choices()) == []


def test_option_render_kw():
F = make_form(
a=SelectField(choices=[("a", "Foo", {"title": "foobar", "data-foo": "bar"})])
)
form = F(a="a")

assert (
'<option data-foo="bar" selected title="foobar" value="a">Foo</option>'
in form.a()
)
assert list(form.a.iter_choices()) == [
("a", "Foo", True, {"title": "foobar", "data-foo": "bar"})
]


def test_optgroup_option_render_kw():
F = make_form(
a=SelectField(
choices={"hello": [("a", "Foo", {"title": "foobar", "data-foo": "bar"})]}
)
)
form = F(a="a")

assert (
'<optgroup label="hello">'
'<option data-foo="bar" selected title="foobar" value="a">Foo</option>'
"</optgroup>" in form.a()
)
assert list(form.a.iter_choices()) == [
("a", "Foo", True, {"title": "foobar", "data-foo": "bar"})
]
43 changes: 39 additions & 4 deletions tests/fields/test_selectmultiple.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ def test_defaults():
# Test for possible regression with null data
form.a.data = None
assert form.validate()
assert list(form.a.iter_choices()) == [(v, l, False) for v, l in form.a.choices]
assert list(form.a.iter_choices()) == [(v, l, False, {}) for v, l in form.a.choices]


def test_with_data():
form = F(DummyPostData(a=["a", "c"]))
assert form.a.data == ["a", "c"]
assert list(form.a.iter_choices()) == [
("a", "hello", True),
("b", "bye", False),
("c", "something", True),
("a", "hello", True, {}),
("b", "bye", False, {}),
("c", "something", True, {}),
]
assert form.b.data == []
form = F(DummyPostData(b=["1", "2"]))
Expand Down Expand Up @@ -149,3 +149,38 @@ def test_render_kw_preserved():
'<option value="bar">bar</option>'
"</select>"
)


def test_option_render_kw():
F = make_form(
a=SelectMultipleField(
choices=[("a", "Foo", {"title": "foobar", "data-foo": "bar"})]
)
)
form = F(a="a")

assert (
'<option data-foo="bar" selected title="foobar" value="a">Foo</option>'
in form.a()
)
assert list(form.a.iter_choices()) == [
("a", "Foo", True, {"title": "foobar", "data-foo": "bar"})
]


def test_optgroup_option_render_kw():
F = make_form(
a=SelectMultipleField(
choices={"hello": [("a", "Foo", {"title": "foobar", "data-foo": "bar"})]}
)
)
form = F(a="a")

assert (
'<optgroup label="hello">'
'<option data-foo="bar" selected title="foobar" value="a">Foo</option>'
"</optgroup>" in form.a()
)
assert list(form.a.iter_choices()) == [
("a", "Foo", True, {"title": "foobar", "data-foo": "bar"})
]