Skip to content

Commit

Permalink
Merge pull request #739 from azmeuk/select-option-customization
Browse files Browse the repository at this point in the history
`SelectField` choice refactoring
  • Loading branch information
azmeuk authored Oct 5, 2023
2 parents 5e0f444 + fada111 commit a6f44f0
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 20 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
- 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"})
]

0 comments on commit a6f44f0

Please sign in to comment.