Skip to content
Open
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: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath('..'))

# -- General configuration -----------------------------------------------------

Expand Down
6 changes: 6 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ code and display format as parameters.
)


GenderField
-----------

.. automodule:: wtforms_components.fields.gender


ColorField
----------

Expand Down
163 changes: 163 additions & 0 deletions tests/fields/test_gender_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
from wtforms import Form

from tests import MultiDict
from wtforms_components import GenderField


def test_no_value():
class TestForm(Form):
gender = GenderField()

form = TestForm()
gender_html = form.gender()
expected_html = (
'<select id="gender" name="gender">'
'<option value="">Not Specified</option>' # noqa
'<option value="male">Male</option>'
'<option value="female">Female</option>'
'<option data-render-as-text value="non-binary">Non-binary</option>'
'</select>'
)
assert gender_html == expected_html


def test_male_value():
class TestForm(Form):
gender = GenderField()

values = MultiDict(gender='male')

form = TestForm(values)
gender_html = form.gender()
expected_html = (
'<select id="gender" name="gender">'
'<option value="">Not Specified</option>' # noqa
'<option selected value="male">Male</option>'
'<option value="female">Female</option>'
'<option data-render-as-text value="non-binary">Non-binary</option>'
'</select>'
)
assert gender_html == expected_html


def test_female_value():
class TestForm(Form):
gender = GenderField()

values = MultiDict(gender='female')

form = TestForm(values)
gender_html = form.gender()
expected_html = (
'<select id="gender" name="gender">'
'<option value="">Not Specified</option>' # noqa
'<option value="male">Male</option>'
'<option selected value="female">Female</option>'
'<option data-render-as-text value="non-binary">Non-binary</option>'
'</select>'
)
assert gender_html == expected_html


def test_non_binary_value():
class TestForm(Form):
gender = GenderField()

values = MultiDict(gender='non-binary')

form = TestForm(values)
gender_html = form.gender()
expected_html = (
'<input id="gender" name="gender" type="text" value="non-binary">'
)
assert gender_html == expected_html


def test_non_simple_value():
class TestForm(Form):
gender = GenderField()

values = MultiDict(gender='transgender')

form = TestForm(values)
gender_html = form.gender()
expected_html = (
'<input id="gender" name="gender" type="text" value="transgender">'
)
assert gender_html == expected_html


def test_customizable_simple_values():
simple_genders = (
('', "Not Specified"),
('female', "Female"),
('male', "Male"),
('trans', "Transgender"),
('non-binary', "Custom"),
)

class TestForm(Form):
gender = GenderField(simple_genders=simple_genders)

form = TestForm()
gender_html = form.gender()
expected_html = (
'<select id="gender" name="gender">'
'<option value="">Not Specified</option>' # noqa
'<option value="female">Female</option>'
'<option value="male">Male</option>'
'<option value="trans">Transgender</option>'
'<option data-render-as-text value="non-binary">Custom</option>'
'</select>'
)
assert gender_html == expected_html


def test_customizable_simple_value_selected():
simple_genders = (
('', "Not Specified"),
('female', "Female"),
('male', "Male"),
('trans', "Transgender"),
('non-binary', "Custom"),
)

class TestForm(Form):
gender = GenderField(simple_genders=simple_genders)

values = MultiDict(gender='trans')

form = TestForm(values)
gender_html = form.gender()
expected_html = (
'<select id="gender" name="gender">'
'<option value="">Not Specified</option>' # noqa
'<option value="female">Female</option>'
'<option value="male">Male</option>'
'<option selected value="trans">Transgender</option>'
'<option data-render-as-text value="non-binary">Custom</option>'
'</select>'
)
assert gender_html == expected_html


def test_customized_non_simple_value():
simple_genders = (
('', "Not Specified"),
('female', "Female"),
('male', "Male"),
('trans', "Transgender"),
('non-binary', "Custom"),
)

class TestForm(Form):
gender = GenderField(simple_genders=simple_genders)

values = MultiDict(gender='genderfluid')

form = TestForm(values)
gender_html = form.gender()
expected_html = (
'<input id="gender" name="gender" type="text" value="genderfluid">'
)
assert gender_html == expected_html
2 changes: 2 additions & 0 deletions wtforms_components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DecimalSliderField,
EmailField,
FloatIntervalField,
GenderField,
IntegerField,
IntegerSliderField,
IntIntervalField,
Expand Down Expand Up @@ -44,6 +45,7 @@
Email,
EmailField,
FloatIntervalField,
GenderField,
If,
IntegerField,
IntegerSliderField,
Expand Down
2 changes: 2 additions & 0 deletions wtforms_components/fields/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .ajax import AjaxField
from .color import ColorField
from .gender import GenderField
from .html5 import (
DateField,
DateTimeField,
Expand Down Expand Up @@ -28,6 +29,7 @@
__all__ = (
AjaxField,
ColorField,
GenderField,
DateField,
DateIntervalField,
DateTimeField,
Expand Down
136 changes: 136 additions & 0 deletions wtforms_components/fields/gender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""
GenderField is a field that represents the gender identity of a person. It is
as inclusive as possible, suggesting "simple" genders like "male" and "female"
but also supporting arbitrary text input for non-binary gender identities.
Note that properly using this field requires a bit of Javascript.

Simple example::

from wtforms import Form
from wtforms_components import GenderField

class UserProfileForm(Form):
gender = GenderField()

To make this field fully functional, you should also add some Javascript to
the page so that when the user selects a non-binary gender, the user is able
to type in their own gender identity.
Here is an example script that you can use:

.. code-block:: javascript

function selectRenderAsText(event) {
var select = event.target;
var option = select.selectedOptions[0];
if("renderAsText" in option.dataset) {
var input = document.createElement("input");
input.type = "text";
input.name = select.name;
input.id = select.id;
input.className = select.className;
input.value = option.value;
select.parentNode.replaceChild(input, select);
input.focus()
};
}
function attachRenderAsTextEvents(event) {
var selects = document.getElementsByTagName("select");
for (var i = 0; i < selects.length; ++i) {
selects[i].addEventListener("change", selectRenderAsText);
}
}
document.addEventListener("DOMContentLoaded", attachRenderAsTextEvents);

Or using jQuery:

.. code-block:: javascript

$(function() {
$("select").change(function() {
if("renderAsText" in $("option:selected", this).data()) {
var input = $("<input>", {
"type": "text",
"name": $(this).attr("name"),
"id": $(this).attr("id"),
"class": $(this).attr("class"),
"value": $(this).val(),
});
$(this).replaceWith(input);
input.focus();
}
});
});

The field includes the following gender options by default:
"Not Specified", "Male", "Female", and "Non-Binary".
To change these options, pass a list of value-label pairs to the
``simple_genders`` parameter when creating this field. For example, to include
"Transgender" in the dropdown, you could do the following::

simple_genders = (
('', "Not Specified"),
('female', "Female"),
('male', "Male"),
('trans', "Transgender"),
('non-binary', "Custom"),
)

class UserProfileForm(Form):
gender = GenderField(simple_genders=simple_genders)

By default, the "Non-Binary" option includes a ``data-render-as-text``
attribute, which indicates to the Javascript on the page that when the
user selects this option, the ``<select>`` element should transform into
an ``<input type="text">`` element so that the user can input an
arbitrary gender identity. To change which option (or options) include
this attribute, pass a list of values to the ``render_as_text`` parameter
when creating this field. For example, to replace the word "Non-binary"
with "Genderqueer", you could do the following::

simple_genders = (
('', "Not Specified"),
('female', "Female"),
('male', "Male"),
('genderqueer', "Genderqueer"),
)

class UserProfileForm(Form):
gender = GenderField(
simple_genders=simple_genders,
render_as_text=['genderqueer'],
)

"""

from ..widgets import GenderWidget
from .html5 import StringField


class GenderField(StringField):
"""
An inclusive gender field. It can render a select field of simple gender
choices, like "male" and "female", or it can render a string field for
non-binary gender choices.
"""
widget = GenderWidget()

def __init__(
self,
label=None,
validators=None,
simple_genders=None,
render_as_text=None,
**kwargs
):
self.simple_genders = simple_genders or [
('', self.gettext('Not Specified')),
('male', self.gettext('Male')),
('female', self.gettext('Female')),
('non-binary', self.gettext('Non-binary')),
]
self.render_as_text = render_as_text or ['non-binary']
super(GenderField, self).__init__(
label=label,
validators=validators,
**kwargs
)
38 changes: 38 additions & 0 deletions wtforms_components/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,41 @@ def render_option(cls, value, label, mixed):
data = (html_params(**options), escape(six.text_type(label)))

return HTMLString(html % data)


class GenderWidget(_Select):
"""
By default, renders a <select> field with simple gender choices. However,
if the field data does not match up with one of these simple choices,
renders an <input type="text"> field, so that the user can view and
submit arbitrary values.
"""
def __call__(self, field, **kwargs):
kwargs.setdefault('id', field.id)

is_simple_gender = any(
field.data == val
for val, label in field.simple_genders
if val not in field.render_as_text
)

if is_simple_gender or not field.data:
# render a <select> field with the simple gender choices
html = ['<select %s>' % html_params(name=field.name, **kwargs)]
for val, label in field.simple_genders:
kwargs = {
"selected": val == field.data
}
if val in field.render_as_text:
kwargs["data-render-as-text"] = True
html.append(self.render_option(val, label, **kwargs))
html.append('</select>')
return HTMLString(''.join(html))
else:
# render an <input type="text"> field for maximum input flexibility
kwargs.setdefault('type', 'text')
if 'value' not in kwargs:
kwargs['value'] = field._value()
return HTMLString('<input %s>' % html_params(
name=field.name, **kwargs
))