diff --git a/CHANGES.rst b/CHANGES.rst index 6723a9a2..79ce1bbe 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,7 @@ Unreleased - Support for optgroups in :class:`~fields.SelectField` and :class:`~fields.SelectMultipleField`. :issue:`656` :pr:`667` - Minor documentation fix. :issue:`701` +- Custom separators for :class:`~fields.FieldList`. :issue:`681` :pr:`694` Version 3.0.0a1 --------------- diff --git a/docs/fields.rst b/docs/fields.rst index 66b7ed33..14524928 100644 --- a/docs/fields.rst +++ b/docs/fields.rst @@ -414,7 +414,7 @@ complex data structures such as lists and nested objects can be represented. :attr:`~wtforms.form.Form.data` dict of the enclosed form. Similarly, the `errors` property encapsulate the forms' errors. -.. autoclass:: FieldList(unbound_field, default field arguments, min_entries=0, max_entries=None) +.. autoclass:: FieldList(unbound_field, default field arguments, min_entries=0, max_entries=None, separator='-') **Note**: Due to a limitation in how HTML sends values, FieldList cannot enclose :class:`BooleanField` or :class:`SubmitField` instances. diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py index 2b049721..71608d36 100644 --- a/src/wtforms/fields/core.py +++ b/src/wtforms/fields/core.py @@ -1105,6 +1105,9 @@ class FieldList(Field): :param max_entries: accept no more than this many entries as input, even if more exist in formdata. + :param separator: + A string which will be suffixed to this field's name to create the + prefix to enclosed list entries. The default is fine for most uses. """ widget = widgets.ListWidget() @@ -1116,6 +1119,7 @@ def __init__( validators=None, min_entries=0, max_entries=None, + separator="-", default=(), **kwargs, ): @@ -1133,6 +1137,8 @@ def __init__( self.max_entries = max_entries self.last_index = -1 self._prefix = kwargs.get("_prefix", "") + self._separator = separator + self._field_separator = unbound_field.kwargs.get("separator", "-") def process(self, formdata, data=unset_value, extra_filters=None): if extra_filters: @@ -1175,12 +1181,12 @@ def _extract_indices(self, prefix, formdata): formdata must be an object which will produce keys when iterated. For example, if field 'foo' contains keys 'foo-0-bar', 'foo-1-baz', then - the numbers 0 and 1 will be yielded, but not neccesarily in order. + the numbers 0 and 1 will be yielded, but not necessarily in order. """ offset = len(prefix) + 1 for k in formdata: if k.startswith(prefix): - k = k[offset:].split("-", 1)[0] + k = k[offset:].split(self._field_separator, 1)[0] if k.isdigit(): yield int(k) @@ -1232,8 +1238,8 @@ def _add_entry(self, formdata=None, data=unset_value, index=None): if index is None: index = self.last_index + 1 self.last_index = index - name = "%s-%d" % (self.short_name, index) - id = "%s-%d" % (self.id, index) + name = f"{self.short_name}{self._separator}{index}" + id = f"{self.id}{self._separator}{index}" field = self.unbound_field.bind( form=None, name=name, diff --git a/tests/fields/test_list.py b/tests/fields/test_list.py index 4df373ea..d04a5d87 100644 --- a/tests/fields/test_list.py +++ b/tests/fields/test_list.py @@ -103,6 +103,68 @@ class Outside(Form): assert o.subforms[0].foo.data == "default" +def test_custom_separator(): + F = make_form(a=FieldList(t, separator="_")) + + pdata = DummyPostData({"a_0": "0_a", "a_1": "1_a"}) + f = F(pdata) + assert f.a[0].data == "0_a" + assert f.a[1].data == "1_a" + + +def test_enclosed_subform_list_separator(): + class Inside(Form): + foo = StringField(default="default") + + class Outside(Form): + subforms = FieldList(FormField(Inside), min_entries=1, separator="_") + + o = Outside() + assert o.subforms[0].foo.data == "default" + assert o.subforms[0].foo.name == "subforms_0-foo" + + pdata = DummyPostData({"subforms_0-foo": "0-foo", "subforms_1-foo": "1-foo"}) + o = Outside(pdata) + assert o.subforms[0].foo.data == "0-foo" + assert o.subforms[1].foo.data == "1-foo" + + +def test_enclosed_subform_uniform_separators(): + class Inside(Form): + foo = StringField(default="default") + + class Outside(Form): + subforms = FieldList( + FormField(Inside, separator="_"), min_entries=1, separator="_" + ) + + o = Outside() + assert o.subforms[0].foo.data == "default" + assert o.subforms[0].foo.name == "subforms_0_foo" + + pdata = DummyPostData({"subforms_0_foo": "0_foo", "subforms_1_foo": "1_foo"}) + o = Outside(pdata) + assert o.subforms[0].foo.data == "0_foo" + assert o.subforms[1].foo.data == "1_foo" + + +def test_enclosed_subform_mixed_separators(): + class Inside(Form): + foo = StringField(default="default") + + class Outside(Form): + subforms = FieldList(FormField(Inside, separator="_"), min_entries=1) + + o = Outside() + assert o.subforms[0].foo.data == "default" + assert o.subforms[0].foo.name == "subforms-0_foo" + + pdata = DummyPostData({"subforms-0_foo": "0_foo", "subforms-1_foo": "1_foo"}) + o = Outside(pdata) + assert o.subforms[0].foo.data == "0_foo" + assert o.subforms[1].foo.data == "1_foo" + + def test_entry_management(): F = make_form(a=FieldList(t)) a = F(a=["hello", "bye"]).a