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

Allow overriding a field's HTML name #601

Merged
merged 5 commits into from
May 31, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Unreleased
- Implemented :class:`~wtforms.fields.core.MonthField`. :pr:`530` :pr:`593`
- Filters can be inline. :meth:`form.BaseForm.process` takes a
*extra_filters* parameter. :issue:`128` :pr:`592`
- Fields can be passed the ``name`` argument to use a HTML name
different than their Python name. :issue:`205`, :pr:`601`


Version 2.3.1
Expand Down
25 changes: 13 additions & 12 deletions src/wtforms/fields/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Field:
do_not_call_in_templates = True # Allow Django 1.4 traversal

def __new__(cls, *args, **kwargs):
if "_form" in kwargs and "_name" in kwargs:
if "_form" in kwargs:
return super().__new__(cls)
else:
return UnboundField(cls, *args, **kwargs)
Expand All @@ -60,8 +60,8 @@ def __init__(
default=None,
widget=None,
render_kw=None,
name=None,
_form=None,
_name=None,
_prefix="",
_translations=None,
_meta=None,
Expand All @@ -88,12 +88,12 @@ def __init__(
:param dict render_kw:
If provided, a dictionary which provides default keywords that
will be given to the widget at render time.
:param name:
The HTML name of this field. The default value is the Python
attribute name.
:param _form:
The form holding this field. It is passed by the form itself during
construction. You should never pass this value yourself.
:param _name:
The name of this field, passed by the enclosing form during its
construction. You should never pass this value yourself.
:param _prefix:
The prefix to prepend to the form name of this field, passed by
the enclosing form during construction.
Expand All @@ -105,7 +105,7 @@ def __init__(
If provided, this is the 'meta' instance from the form. You usually
don't pass this yourself.

If `_form` and `_name` isn't provided, an :class:`UnboundField` will be
If `_form` isn't provided, an :class:`UnboundField` will be
returned instead. Call its :func:`bind` method with a form instance and
a name to construct the field.
"""
Expand All @@ -124,8 +124,8 @@ def __init__(
self.render_kw = render_kw
self.filters = filters
self.flags = Flags()
self.name = _prefix + _name
self.short_name = _name
self.name = _prefix + name
self.short_name = name
self.type = type(self).__name__

self.check_validators(validators)
Expand All @@ -136,7 +136,7 @@ def __init__(
self.id,
label
if label is not None
else self.gettext(_name.replace("_", " ").title()),
else self.gettext(name.replace("_", " ").title()),
)

if widget is not None:
Expand Down Expand Up @@ -378,10 +378,11 @@ class UnboundField:
_formfield = True
creation_counter = 0

def __init__(self, field_class, *args, **kwargs):
def __init__(self, field_class, *args, name=None, **kwargs):
UnboundField.creation_counter += 1
self.field_class = field_class
self.args = args
self.name = name
self.kwargs = kwargs
self.creation_counter = UnboundField.creation_counter
validators = kwargs.get("validators")
Expand All @@ -391,9 +392,9 @@ def __init__(self, field_class, *args, **kwargs):
def bind(self, form, name, prefix="", translations=None, **kwargs):
kw = dict(
self.kwargs,
name=name,
_form=form,
_prefix=prefix,
_name=name,
_translations=translations,
**kwargs,
)
Expand Down Expand Up @@ -479,7 +480,7 @@ def iter_choices(self):

def __iter__(self):
opts = dict(
widget=self.option_widget, _name=self.name, _form=None, _meta=self.meta
widget=self.option_widget, name=self.name, _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)
Expand Down
99 changes: 50 additions & 49 deletions src/wtforms/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def __init__(self, fields, prefix="", meta=_default_meta):
extra_fields.extend(self._csrf.setup_form(self))

for name, unbound_field in itertools.chain(fields, extra_fields):
options = dict(name=name, prefix=prefix, translations=translations)
field_name = unbound_field.name or name
options = dict(name=field_name, prefix=prefix, translations=translations)
field = meta.bind_field(self, unbound_field, options)
self._fields[name] = field

Expand Down Expand Up @@ -80,35 +81,30 @@ def populate_obj(self, obj):
field.populate_obj(obj, name)

def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwargs):
"""
Take form, object data, and keyword arg input and have the fields
process them.

:param formdata:
Used to pass data coming from the enduser, usually `request.POST` or
equivalent.
:param obj:
If `formdata` is empty or not provided, this object is checked for
attributes matching form field names, which will be used for field
values.
:param data:
If provided, must be a dictionary of data. This is only used if
`formdata` is empty or not provided and `obj` does not contain
an attribute named the same as the field.
:param extra_filters: A dict mapping field names to lists of
extra filters functions to run. Extra filters run after
filters passed when creating the field. If the form has
``filter_<fieldname>``, it is the last extra filter.
:param `**kwargs`:
If `formdata` is empty or not provided and `obj` does not contain
an attribute named the same as a field, form will assign the value
of a matching keyword argument to the field, if one exists.
"""Process default and input data with each field.

:param formdata: Input data coming from the client, usually
``request.form`` or equivalent. Should provide a "multi
dict" interface to get a list of values for a given key,
such as what Werkzeug, Django, and WebOb provide.
:param obj: Take existing data from attributes on this object
matching form field attributes. Only used if ``formdata`` is
not passed.
:param data: Take existing data from keys in this dict matching
form field attributes. ``obj`` takes precedence if it also
has a matching attribute. Only used if ``formdata`` is not
passed.
:param extra_filters: A dict mapping field attribute names to
lists of extra filter functions to run. Extra filters run
after filters passed when creating the field. If the form
has ``filter_<fieldname>``, it is the last extra filter.
:param kwargs: Merged with ``data`` to allow passing existing
data as parameters. Overwrites any duplicate keys in
``data``. Only used if ``formdata`` is not passed.
"""
formdata = self.meta.wrap_formdata(self, formdata)

if data is not None:
# XXX we want to eventually process 'data' as a new entity.
# Temporarily, this can simply be merged with kwargs.
kwargs = dict(data, **kwargs)

filters = extra_filters.copy() if extra_filters is not None else {}
Expand All @@ -125,7 +121,9 @@ def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwar
formdata, getattr(obj, name), extra_filters=field_extra_filters
)
elif name in kwargs:
field.process(formdata, kwargs[name], extra_filters=field_extra_filters)
field.process(
formdata, kwargs[name], extra_filters=field_extra_filters,
)
else:
field.process(formdata, extra_filters=field_extra_filters)

Expand Down Expand Up @@ -245,28 +243,31 @@ def __init__(
self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs,
):
"""
:param formdata:
Used to pass data coming from the enduser, usually `request.POST` or
equivalent. formdata should be some sort of request-data wrapper which
can get multiple parameters from the form input, and values are unicode
strings, e.g. a Werkzeug/Django/WebOb MultiDict
:param obj:
If `formdata` is empty or not provided, this object is checked for
attributes matching form field names, which will be used for field
values.
:param prefix:
If provided, all fields will have their name prefixed with the
value.
:param data:
Accept a dictionary of data. This is only used if `formdata` and
`obj` are not present.
:param meta:
If provided, this is a dictionary of values to override attributes
on this form's meta instance.
:param `**kwargs`:
If `formdata` is empty or not provided and `obj` does not contain
an attribute named the same as a field, form will assign the value
of a matching keyword argument to the field, if one exists.
:param formdata: Input data coming from the client, usually
``request.form`` or equivalent. Should provide a "multi
dict" interface to get a list of values for a given key,
such as what Werkzeug, Django, and WebOb provide.
:param obj: Take existing data from attributes on this object
matching form field attributes. Only used if ``formdata`` is
not passed.
:param prefix: If provided, all fields will have their name
prefixed with the value. This is for distinguishing multiple
forms on a single page. This only affects the HTML name for
matching input data, not the Python name for matching
existing data.
:param data: Take existing data from keys in this dict matching
form field attributes. ``obj`` takes precedence if it also
has a matching attribute. Only used if ``formdata`` is not
passed.
:param meta: A dict of attributes to override on this form's
:attr:`meta` instance.
:param extra_filters: A dict mapping field attribute names to
lists of extra filter functions to run. Extra filters run
after filters passed when creating the field. If the form
has ``filter_<fieldname>``, it is the last extra filter.
:param kwargs: Merged with ``data`` to allow passing existing
data as parameters. Overwrites any duplicate keys in
``data``. Only used if ``formdata`` is not passed.
"""
meta_obj = self._wtforms_meta()
if meta is not None and isinstance(meta, dict):
Expand Down
63 changes: 58 additions & 5 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def __init__(self, *args, **kw):
self.__dict__.update(*args, **kw)


def make_form(_name="F", **fields):
return type(str(_name), (Form,), fields)
def make_form(name="F", **fields):
return type(str(name), (Form,), fields)


class TestDefaults:
Expand Down Expand Up @@ -244,12 +244,12 @@ def test_meta_attribute(self):

# Can we pass in meta via _meta?
form_meta = meta.DefaultMeta()
field = StringField(_name="Foo", _form=None, _meta=form_meta)
field = StringField(name="Foo", _form=None, _meta=form_meta)
assert field.meta is form_meta

# Do we fail if both _meta and _form are None?
with pytest.raises(TypeError):
StringField(_name="foo", _form=None)
StringField(name="foo", _form=None)

def test_render_kw(self):
form = self.F()
Expand Down Expand Up @@ -309,6 +309,41 @@ def test_check_validators(self):
):
Field(validators=[v2])

def test_custom_name(self):
class F(Form):
foo = StringField(name="bar", default="default")
x = StringField()

class ObjFoo:
foo = "obj"

class ObjBar:
bar = "obj"

f = F(DummyPostData(foo="data"))
assert f.foo.data == "default"
assert 'value="default"' in f.foo()

f = F(DummyPostData(bar="data"))
assert f.foo.data == "data"
assert 'value="data"' in f.foo()

f = F(foo="kwarg")
assert f.foo.data == "kwarg"
assert 'value="kwarg"' in f.foo()

f = F(bar="kwarg")
assert f.foo.data == "default"
assert 'value="default"' in f.foo()

f = F(obj=ObjFoo())
assert f.foo.data == "obj"
assert 'value="obj"' in f.foo()

f = F(obj=ObjBar())
assert f.foo.data == "default"
assert 'value="default"' in f.foo()


class PrePostTestField(StringField):
def pre_validate(self, form):
Expand Down Expand Up @@ -1087,6 +1122,24 @@ def make_inner():
with pytest.raises(TypeError):
form.populate_obj(obj2)

def test_enclosed_subform_custom_name(self):
class Inside(Form):
foo = StringField(name="bar", default="default")

class Outside(Form):
subforms = FieldList(FormField(Inside), min_entries=1)

o = Outside()
assert o.subforms[0].foo.data == "default"

pdata = DummyPostData({"subforms-0-bar": "form"})
o = Outside(pdata)
assert o.subforms[0].foo.data == "form"

pdata = DummyPostData({"subforms-0-foo": "form"})
o = Outside(pdata)
assert o.subforms[0].foo.data == "default"

def test_entry_management(self):
F = make_form(a=FieldList(self.t))
a = F(a=["hello", "bye"]).a
Expand Down Expand Up @@ -1142,7 +1195,7 @@ def validator(form, field):

def test_no_filters(self):
with pytest.raises(TypeError):
FieldList(self.t, filters=[lambda x: x], _form=Form(), _name="foo")
FieldList(self.t, filters=[lambda x: x], _form=Form(), name="foo")

def test_process_prefilled(self):
data = ["foo", "hi", "rawr"]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_locale_babel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ def build(**kw):
form = self.F()
DecimalField(
use_locale=True,
name="a",
_form=form,
_name="a",
_translations=form.meta.get_translations(form),
**kw
)
Expand Down