diff --git a/docs/conf.py b/docs/conf.py index b45a45b..b5031a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 ----------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 28a4154..1387ba5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -102,6 +102,12 @@ code and display format as parameters. ) +GenderField +----------- + +.. automodule:: wtforms_components.fields.gender + + ColorField ---------- diff --git a/tests/fields/test_gender_field.py b/tests/fields/test_gender_field.py new file mode 100644 index 0000000..b5b9827 --- /dev/null +++ b/tests/fields/test_gender_field.py @@ -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 = ( + '' + ) + 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 = ( + '' + ) + 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 = ( + '' + ) + 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 = ( + '' + ) + 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 = ( + '' + ) + 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 = ( + '' + ) + 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 = ( + '' + ) + 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 = ( + '' + ) + assert gender_html == expected_html diff --git a/wtforms_components/__init__.py b/wtforms_components/__init__.py index e8fdcc5..b2b4370 100644 --- a/wtforms_components/__init__.py +++ b/wtforms_components/__init__.py @@ -12,6 +12,7 @@ DecimalSliderField, EmailField, FloatIntervalField, + GenderField, IntegerField, IntegerSliderField, IntIntervalField, @@ -44,6 +45,7 @@ Email, EmailField, FloatIntervalField, + GenderField, If, IntegerField, IntegerSliderField, diff --git a/wtforms_components/fields/__init__.py b/wtforms_components/fields/__init__.py index 5dd504b..8852c94 100644 --- a/wtforms_components/fields/__init__.py +++ b/wtforms_components/fields/__init__.py @@ -1,5 +1,6 @@ from .ajax import AjaxField from .color import ColorField +from .gender import GenderField from .html5 import ( DateField, DateTimeField, @@ -28,6 +29,7 @@ __all__ = ( AjaxField, ColorField, + GenderField, DateField, DateIntervalField, DateTimeField, diff --git a/wtforms_components/fields/gender.py b/wtforms_components/fields/gender.py new file mode 100644 index 0000000..1cb76f3 --- /dev/null +++ b/wtforms_components/fields/gender.py @@ -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 = $("", { + "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 ```` 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 + ) diff --git a/wtforms_components/widgets.py b/wtforms_components/widgets.py index ec19665..4303967 100644 --- a/wtforms_components/widgets.py +++ b/wtforms_components/widgets.py @@ -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 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 ' % 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('') + return HTMLString(''.join(html)) + else: + # render an field for maximum input flexibility + kwargs.setdefault('type', 'text') + if 'value' not in kwargs: + kwargs['value'] = field._value() + return HTMLString('' % html_params( + name=field.name, **kwargs + ))