Skip to content

Commit

Permalink
Merge pull request #601 from azmeuk/issue-205-override-field-name
Browse files Browse the repository at this point in the history
Allow overriding a field's HTML name
  • Loading branch information
davidism authored May 31, 2020
2 parents 730afca + cdc4e6b commit 3149b38
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 67 deletions.
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

0 comments on commit 3149b38

Please sign in to comment.