Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Fixed #19686 -- Added number HTML5 input type for numeric form fields #696

Closed
wants to merge 1 commit into from

2 participants

@claudep
Collaborator

No description provided.

@charettes charettes commented on the diff
django/forms/fields.py
@@ -234,6 +234,7 @@ class IntegerField(Field):
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
+ kwargs.setdefault('widget', NumberInput if not kwargs.get('localize') else self.widget)
@charettes Collaborator

I would favor the widget property approach since altering the localize attribute after instantiation won't work. I agree it's really and edge case but I think both of those declaration should be equivalent:

IntegerField(localize=True)
f = IntegerField()
f.localize = True

In the case of the f.localize = True you have f.widget.__class__ is NumberInput which is exactly what we're trying to avoid here.

I know setting the localize attribute at that time is not documented but I'm pretty sure the following pattern has been used in the wild (I've been using it :) since you must specify it even if USE_L10N = True.

class LocalizedForm(forms.Form):
    def __init__(self, *args, *kwargs):
        super(LocalizedForm, self).__init__(*args, **kwargs)
        localize = settings.USE_L10N
        for field in self.fields.values():
            field.localize = localize
@claudep Collaborator
claudep added a note

May I suggest you write your alternative as a patch, so as I can better realize the pros and cons of both variants?

@charettes Collaborator

I'll take the time to write one later today but we could use a @property:

@property
def widget(self):
    if self.localize:
        return TextInput
    else:
        return NumberInput

One of the con here is that NumberField.widget will be an instance of property. We could also write a descriptor to maintain backward compatibilty:

class WidgetDescriptor(object):
    def __init__(self, widget, localized_widget):
        self.widget = widget
        self.localized_widget = localized_widget

    def __get__(self, instance, owner):
        if instance and instance.localize:
            return self.localized_widget
         return self.widget

class IntegerField(Field):
    widget = WidgetDescriptor(NumberInput, TextInput)

Maybe I'm just over-complicating this whole thing.

@charettes Collaborator

While trying to write a patch I figured out settings up localize after instantiation doesn't alter the underlying widget's is_localized property. Plus I realized I was actually using a custom ModelAdmin with an overriden formfield_for_dbfield to handle what was described in the LocalizedForm example, so we're not actually breaking anything as far as I can tell.

For this reason I retract my concern here, the setdefault approach seems to be the right way to do this.

I'll attach a patch with additional tests and the DecimalField.widget_attrs issue fixed at https://code.djangoproject.com/ticket/19686.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@charettes charettes commented on the diff
django/forms/fields.py
@@ -345,6 +355,16 @@ def validate(self, value):
raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
return value
+ def widget_attrs(self, widget):
+ attrs = super(DecimalField, self).widget_attrs(widget)
+ if isinstance(widget, NumberInput):
+ if self.max_digits is not None:
+ attrs['maxlength'] = self.max_digits + 1 # for the dot
@charettes Collaborator

This must also take the sign into account. What about:

max_length = self.max_digits + 1 # for the sign
if self.decimal_places is None or self.decimal_places > 0:
    max_length += 1 # for the dot

We could also make the sign check conditional based on min_value and max_value but it would be a mess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@claudep
Collaborator

Simon attached a revised patch on the ticket.

@claudep claudep closed this
@claudep claudep deleted the claudep:19686 branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
42 django/forms/fields.py
@@ -19,7 +19,7 @@
from django.core.exceptions import ValidationError
from django.forms.util import ErrorList, from_current_timezone, to_current_timezone
from django.forms.widgets import (
- TextInput, PasswordInput, EmailInput, URLInput, HiddenInput,
+ TextInput, NumberInput, PasswordInput, EmailInput, URLInput, HiddenInput,
MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select,
NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput,
SplitDateTimeWidget, SplitHiddenDateTimeWidget, FILE_INPUT_CONTRADICTION
@@ -234,6 +234,7 @@ class IntegerField(Field):
def __init__(self, max_value=None, min_value=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
+ kwargs.setdefault('widget', NumberInput if not kwargs.get('localize') else self.widget)
@charettes Collaborator

I would favor the widget property approach since altering the localize attribute after instantiation won't work. I agree it's really and edge case but I think both of those declaration should be equivalent:

IntegerField(localize=True)
f = IntegerField()
f.localize = True

In the case of the f.localize = True you have f.widget.__class__ is NumberInput which is exactly what we're trying to avoid here.

I know setting the localize attribute at that time is not documented but I'm pretty sure the following pattern has been used in the wild (I've been using it :) since you must specify it even if USE_L10N = True.

class LocalizedForm(forms.Form):
    def __init__(self, *args, *kwargs):
        super(LocalizedForm, self).__init__(*args, **kwargs)
        localize = settings.USE_L10N
        for field in self.fields.values():
            field.localize = localize
@claudep Collaborator
claudep added a note

May I suggest you write your alternative as a patch, so as I can better realize the pros and cons of both variants?

@charettes Collaborator

I'll take the time to write one later today but we could use a @property:

@property
def widget(self):
    if self.localize:
        return TextInput
    else:
        return NumberInput

One of the con here is that NumberField.widget will be an instance of property. We could also write a descriptor to maintain backward compatibilty:

class WidgetDescriptor(object):
    def __init__(self, widget, localized_widget):
        self.widget = widget
        self.localized_widget = localized_widget

    def __get__(self, instance, owner):
        if instance and instance.localize:
            return self.localized_widget
         return self.widget

class IntegerField(Field):
    widget = WidgetDescriptor(NumberInput, TextInput)

Maybe I'm just over-complicating this whole thing.

@charettes Collaborator

While trying to write a patch I figured out settings up localize after instantiation doesn't alter the underlying widget's is_localized property. Plus I realized I was actually using a custom ModelAdmin with an overriden formfield_for_dbfield to handle what was described in the LocalizedForm example, so we're not actually breaking anything as far as I can tell.

For this reason I retract my concern here, the setdefault approach seems to be the right way to do this.

I'll attach a patch with additional tests and the DecimalField.widget_attrs issue fixed at https://code.djangoproject.com/ticket/19686.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
super(IntegerField, self).__init__(*args, **kwargs)
if max_value is not None:
@@ -257,6 +258,16 @@ def to_python(self, value):
raise ValidationError(self.error_messages['invalid'])
return value
+ def widget_attrs(self, widget):
+ attrs = super(IntegerField, self).widget_attrs(widget)
+ if isinstance(widget, NumberInput):
+ if self.min_value is not None:
+ attrs['min'] = self.min_value
+ if self.max_value is not None:
+ attrs['max'] = self.max_value
+ return attrs
+
+
class FloatField(IntegerField):
default_error_messages = {
'invalid': _('Enter a number.'),
@@ -278,25 +289,24 @@ def to_python(self, value):
raise ValidationError(self.error_messages['invalid'])
return value
-class DecimalField(Field):
+ def widget_attrs(self, widget):
+ attrs = super(FloatField, self).widget_attrs(widget)
+ if isinstance(widget, NumberInput):
+ attrs.setdefault('step', 'any')
+ return attrs
+
+
+class DecimalField(IntegerField):
default_error_messages = {
'invalid': _('Enter a number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
'max_digits': _('Ensure that there are no more than %s digits in total.'),
'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
}
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
self.max_digits, self.decimal_places = max_digits, decimal_places
- Field.__init__(self, *args, **kwargs)
-
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
+ super(DecimalField, self).__init__(max_value, min_value, *args, **kwargs)
def to_python(self, value):
"""
@@ -345,6 +355,16 @@ def validate(self, value):
raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
return value
+ def widget_attrs(self, widget):
+ attrs = super(DecimalField, self).widget_attrs(widget)
+ if isinstance(widget, NumberInput):
+ if self.max_digits is not None:
+ attrs['maxlength'] = self.max_digits + 1 # for the dot
@charettes Collaborator

This must also take the sign into account. What about:

max_length = self.max_digits + 1 # for the sign
if self.decimal_places is None or self.decimal_places > 0:
    max_length += 1 # for the dot

We could also make the sign check conditional based on min_value and max_value but it would be a mess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ if self.decimal_places:
+ attrs['step'] = '0.%s1' % ('0' * (self.decimal_places-1))
+ return attrs
+
+
class BaseTemporalField(Field):
def __init__(self, input_formats=None, *args, **kwargs):
View
6 django/forms/widgets.py
@@ -23,7 +23,7 @@
__all__ = (
'Media', 'MediaDefiningClass', 'Widget', 'TextInput',
- 'EmailInput', 'URLInput', 'PasswordInput',
+ 'EmailInput', 'URLInput', 'NumberInput', 'PasswordInput',
'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput',
'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
@@ -252,6 +252,10 @@ def __init__(self, attrs=None):
super(TextInput, self).__init__(attrs)
+class NumberInput(TextInput):
+ input_type = 'number'
+
+
class EmailInput(TextInput):
input_type = 'email'
View
9 docs/ref/forms/fields.txt
@@ -454,7 +454,8 @@ For each field, we describe the default widget used if you don't specify
.. class:: DecimalField(**kwargs)
- * Default widget: :class:`TextInput`
+ * Default widget: :class:`NumberInput` when :attr:`Field.localize` is
+ ``False``, else :class:`TextInput`.
* Empty value: ``None``
* Normalizes to: A Python ``decimal``.
* Validates that the given value is a decimal. Leading and trailing
@@ -580,7 +581,8 @@ For each field, we describe the default widget used if you don't specify
.. class:: FloatField(**kwargs)
- * Default widget: :class:`TextInput`
+ * Default widget: :class:`NumberInput` when :attr:`Field.localize` is
+ ``False``, else :class:`TextInput`.
* Empty value: ``None``
* Normalizes to: A Python float.
* Validates that the given value is an float. Leading and trailing
@@ -621,7 +623,8 @@ For each field, we describe the default widget used if you don't specify
.. class:: IntegerField(**kwargs)
- * Default widget: :class:`TextInput`
+ * Default widget: :class:`NumberInput` when :attr:`Field.localize` is
+ ``False``, else :class:`TextInput`.
* Empty value: ``None``
* Normalizes to: A Python integer or long integer.
* Validates that the given value is an integer. Leading and trailing
View
13 docs/ref/forms/widgets.txt
@@ -394,6 +394,19 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
Text input: ``<input type="text" ...>``
+``NumberInput``
+~~~~~~~~~~~~~~~
+
+.. class:: NumberInput
+
+ .. versionadded:: 1.6
+
+ Text input: ``<input type="number" ...>``
+
+ Beware that not all browsers support entering localized numbers in
+ ``number`` input types. Django itself avoids using them for fields having
+ their :attr:`~django.forms.Field.localize` property to ``True``.
+
``EmailInput``
~~~~~~~~~~~~~~
View
13 docs/releases/1.6.txt
@@ -31,9 +31,13 @@ Minor features
* Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with
:meth:`~django.db.models.query.QuerySet.latest`.
-* The default widgets for :class:`~django.forms.EmailField` and
- :class:`~django.forms.URLField` use the new type attributes available in
- HTML5 (type='email', type='url').
+* The default widgets for :class:`~django.forms.EmailField`,
+ :class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`,
+ :class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use
+ the new type attributes available in HTML5 (type='email', type='url',
+ type='number'). Note that due to erratic support of the ``number`` input type
+ with localized numbers in current browsers, Django only uses it when numeric
+ fields are not localized.
* The ``number`` argument for :ref:`lazy plural translations
<lazy-plural-translations>` can be provided at translation time rather than
@@ -49,7 +53,8 @@ Backwards incompatible changes in 1.6
* If your CSS/Javascript code used to access HTML input widgets by type, you
should review it as ``type='text'`` widgets might be now output as
- ``type='email'`` or ``type='url'`` depending on their corresponding field type.
+ ``type='email'``, ``type='url'`` or ``type='number'`` depending on their
+ corresponding field type.
* Extraction of translatable literals from templates with the
:djadmin:`makemessages` command now correctly detects i18n constructs when
View
6 docs/topics/forms/formsets.txt
@@ -273,13 +273,13 @@ Lets you create a formset with the ability to order::
... print(form.as_table())
<tr><th><label for="id_form-0-title">Title:</label></th><td><input type="text" name="form-0-title" value="Article #1" id="id_form-0-title" /></td></tr>
<tr><th><label for="id_form-0-pub_date">Pub date:</label></th><td><input type="text" name="form-0-pub_date" value="2008-05-10" id="id_form-0-pub_date" /></td></tr>
- <tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="text" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
+ <tr><th><label for="id_form-0-ORDER">Order:</label></th><td><input type="number" name="form-0-ORDER" value="1" id="id_form-0-ORDER" /></td></tr>
<tr><th><label for="id_form-1-title">Title:</label></th><td><input type="text" name="form-1-title" value="Article #2" id="id_form-1-title" /></td></tr>
<tr><th><label for="id_form-1-pub_date">Pub date:</label></th><td><input type="text" name="form-1-pub_date" value="2008-05-11" id="id_form-1-pub_date" /></td></tr>
- <tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="text" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
+ <tr><th><label for="id_form-1-ORDER">Order:</label></th><td><input type="number" name="form-1-ORDER" value="2" id="id_form-1-ORDER" /></td></tr>
<tr><th><label for="id_form-2-title">Title:</label></th><td><input type="text" name="form-2-title" id="id_form-2-title" /></td></tr>
<tr><th><label for="id_form-2-pub_date">Pub date:</label></th><td><input type="text" name="form-2-pub_date" id="id_form-2-pub_date" /></td></tr>
- <tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="text" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>
+ <tr><th><label for="id_form-2-ORDER">Order:</label></th><td><input type="number" name="form-2-ORDER" id="id_form-2-ORDER" /></td></tr>
This adds an additional field to each form. This new field is named ``ORDER``
and is an ``forms.IntegerField``. For the forms that came from the initial
View
4 tests/modeltests/model_forms/tests.py
@@ -1133,7 +1133,7 @@ def test_with_data(self):
<option value="%s">Joe Better</option>
<option value="%s">Mike Royko</option>
</select></p>
-<p><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p>''' % (w_woodward.pk, w_bernstein.pk, bw.pk, w_royko.pk))
+<p><label for="id_age">Age:</label> <input type="number" name="age" id="id_age" min="0" /></p>''' % (w_woodward.pk, w_bernstein.pk, bw.pk, w_royko.pk))
data = {
'writer': six.text_type(w_woodward.pk),
@@ -1151,7 +1151,7 @@ def test_with_data(self):
<option value="%s">Joe Better</option>
<option value="%s">Mike Royko</option>
</select></p>
-<p><label for="id_age">Age:</label> <input type="text" name="age" value="65" id="id_age" /></p>''' % (w_woodward.pk, w_bernstein.pk, bw.pk, w_royko.pk))
+<p><label for="id_age">Age:</label> <input type="number" name="age" value="65" id="id_age" min="0" /></p>''' % (w_woodward.pk, w_bernstein.pk, bw.pk, w_royko.pk))
def test_file_field(self):
# Test conditions when files is either not given or empty.
View
17 tests/modeltests/model_formsets/tests.py
@@ -384,7 +384,7 @@ def test_model_inheritance(self):
self.assertEqual(len(formset.forms), 1)
self.assertHTMLEqual(formset.forms[0].as_p(),
'<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></p>\n'
- '<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" id="id_form-0-author_ptr" /></p>')
+ '<p><label for="id_form-0-write_speed">Write speed:</label> <input type="number" name="form-0-write_speed" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" id="id_form-0-author_ptr" /></p>')
data = {
'form-TOTAL_FORMS': '1', # the number of forms rendered
@@ -407,10 +407,10 @@ def test_model_inheritance(self):
self.assertEqual(len(formset.forms), 2)
self.assertHTMLEqual(formset.forms[0].as_p(),
'<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Ernest Hemingway" maxlength="100" /></p>\n'
- '<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" value="10" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" value="%d" id="id_form-0-author_ptr" /></p>' % hemingway_id)
+ '<p><label for="id_form-0-write_speed">Write speed:</label> <input type="number" name="form-0-write_speed" value="10" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" value="%d" id="id_form-0-author_ptr" /></p>' % hemingway_id)
self.assertHTMLEqual(formset.forms[1].as_p(),
'<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /></p>\n'
- '<p><label for="id_form-1-write_speed">Write speed:</label> <input type="text" name="form-1-write_speed" id="id_form-1-write_speed" /><input type="hidden" name="form-1-author_ptr" id="id_form-1-author_ptr" /></p>')
+ '<p><label for="id_form-1-write_speed">Write speed:</label> <input type="number" name="form-1-write_speed" id="id_form-1-write_speed" /><input type="hidden" name="form-1-author_ptr" id="id_form-1-author_ptr" /></p>')
data = {
'form-TOTAL_FORMS': '2', # the number of forms rendered
@@ -543,6 +543,7 @@ def test_inline_formsets_save_as_new(self):
def test_inline_formsets_with_custom_pk(self):
# Test inline formsets where the inline-edited object has a custom
# primary key that is not the fk to the parent object.
+ self.maxDiff = 1024
AuthorBooksFormSet2 = inlineformset_factory(Author, BookWithCustomPK, can_delete=False, extra=1)
author = Author.objects.create(pk=1, name='Charles Baudelaire')
@@ -550,7 +551,7 @@ def test_inline_formsets_with_custom_pk(self):
formset = AuthorBooksFormSet2(instance=author)
self.assertEqual(len(formset.forms), 1)
self.assertHTMLEqual(formset.forms[0].as_p(),
- '<p><label for="id_bookwithcustompk_set-0-my_pk">My pk:</label> <input type="text" name="bookwithcustompk_set-0-my_pk" id="id_bookwithcustompk_set-0-my_pk" /></p>\n'
+ '<p><label for="id_bookwithcustompk_set-0-my_pk">My pk:</label> <input id="id_bookwithcustompk_set-0-my_pk" type="number" name="bookwithcustompk_set-0-my_pk" maxlength="6" /></p>\n'
'<p><label for="id_bookwithcustompk_set-0-title">Title:</label> <input id="id_bookwithcustompk_set-0-title" type="text" name="bookwithcustompk_set-0-title" maxlength="100" /><input type="hidden" name="bookwithcustompk_set-0-author" value="1" id="id_bookwithcustompk_set-0-author" /></p>')
data = {
@@ -798,7 +799,7 @@ def test_custom_pk(self):
'<option value="%d">Joe Perry at Giordanos</option>\n'
'<option value="%d">Jack Berry at Giordanos</option>\n'
'</select></p>\n'
- '<p><label for="id_form-0-age">Age:</label> <input type="text" name="form-0-age" id="id_form-0-age" /></p>'
+ '<p><label for="id_form-0-age">Age:</label> <input type="number" name="form-0-age" id="id_form-0-age" min="0" /></p>'
% (owner1.auto_id, owner2.auto_id))
owner1 = Owner.objects.get(name='Joe Perry')
@@ -808,7 +809,7 @@ def test_custom_pk(self):
formset = FormSet(instance=owner1)
self.assertEqual(len(formset.forms), 1)
self.assertHTMLEqual(formset.forms[0].as_p(),
- '<p><label for="id_ownerprofile-0-age">Age:</label> <input type="text" name="ownerprofile-0-age" id="id_ownerprofile-0-age" /><input type="hidden" name="ownerprofile-0-owner" value="%d" id="id_ownerprofile-0-owner" /></p>'
+ '<p><label for="id_ownerprofile-0-age">Age:</label> <input type="number" name="ownerprofile-0-age" id="id_ownerprofile-0-age" min="0" /><input type="hidden" name="ownerprofile-0-owner" value="%d" id="id_ownerprofile-0-owner" /></p>'
% owner1.auto_id)
data = {
@@ -829,7 +830,7 @@ def test_custom_pk(self):
formset = FormSet(instance=owner1)
self.assertEqual(len(formset.forms), 1)
self.assertHTMLEqual(formset.forms[0].as_p(),
- '<p><label for="id_ownerprofile-0-age">Age:</label> <input type="text" name="ownerprofile-0-age" value="54" id="id_ownerprofile-0-age" /><input type="hidden" name="ownerprofile-0-owner" value="%d" id="id_ownerprofile-0-owner" /></p>'
+ '<p><label for="id_ownerprofile-0-age">Age:</label> <input type="number" name="ownerprofile-0-age" value="54" id="id_ownerprofile-0-age" min="0" /><input type="hidden" name="ownerprofile-0-owner" value="%d" id="id_ownerprofile-0-owner" /></p>'
% owner1.auto_id)
data = {
@@ -985,7 +986,7 @@ def test_callable_defaults(self):
result = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?', '__DATETIME__', result)
self.assertHTMLEqual(result,
'<p><label for="id_membership_set-0-date_joined">Date joined:</label> <input type="text" name="membership_set-0-date_joined" value="__DATETIME__" id="id_membership_set-0-date_joined" /><input type="hidden" name="initial-membership_set-0-date_joined" value="__DATETIME__" id="initial-membership_set-0-id_membership_set-0-date_joined" /></p>\n'
- '<p><label for="id_membership_set-0-karma">Karma:</label> <input type="text" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-person" value="%d" id="id_membership_set-0-person" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>'
+ '<p><label for="id_membership_set-0-karma">Karma:</label> <input type="number" name="membership_set-0-karma" id="id_membership_set-0-karma" /><input type="hidden" name="membership_set-0-person" value="%d" id="id_membership_set-0-person" /><input type="hidden" name="membership_set-0-id" id="id_membership_set-0-id" /></p>'
% person.id)
# test for validation with callable defaults. Validations rely on hidden fields
View
8 tests/regressiontests/forms/tests/fields.py
@@ -131,6 +131,7 @@ def test_charfield_widget_attrs(self):
def test_integerfield_1(self):
f = IntegerField()
+ self.assertWidgetRendersTo(f, '<input type="number" name="f" id="id_f" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertEqual(1, f.clean('1'))
@@ -165,6 +166,7 @@ def test_integerfield_2(self):
def test_integerfield_3(self):
f = IntegerField(max_value=10)
+ self.assertWidgetRendersTo(f, '<input max="10" type="number" name="f" id="id_f" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertEqual(1, f.clean(1))
self.assertEqual(10, f.clean(10))
@@ -176,6 +178,7 @@ def test_integerfield_3(self):
def test_integerfield_4(self):
f = IntegerField(min_value=10)
+ self.assertWidgetRendersTo(f, '<input id="id_f" type="number" name="f" min="10" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertRaisesMessage(ValidationError, "'Ensure this value is greater than or equal to 10.'", f.clean, 1)
self.assertEqual(10, f.clean(10))
@@ -187,6 +190,7 @@ def test_integerfield_4(self):
def test_integerfield_5(self):
f = IntegerField(min_value=10, max_value=20)
+ self.assertWidgetRendersTo(f, '<input id="id_f" max="20" type="number" name="f" min="10" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertRaisesMessage(ValidationError, "'Ensure this value is greater than or equal to 10.'", f.clean, 1)
self.assertEqual(10, f.clean(10))
@@ -202,6 +206,7 @@ def test_integerfield_5(self):
def test_floatfield_1(self):
f = FloatField()
+ self.assertWidgetRendersTo(f, '<input step="any" type="number" name="f" id="id_f" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertEqual(1.0, f.clean('1'))
@@ -228,6 +233,7 @@ def test_floatfield_2(self):
def test_floatfield_3(self):
f = FloatField(max_value=1.5, min_value=0.5)
+ self.assertWidgetRendersTo(f, '<input step="any" name="f" min="0.5" max="1.5" type="number" id="id_f" />')
self.assertRaisesMessage(ValidationError, "'Ensure this value is less than or equal to 1.5.'", f.clean, '1.6')
self.assertRaisesMessage(ValidationError, "'Ensure this value is greater than or equal to 0.5.'", f.clean, '0.4')
self.assertEqual(1.5, f.clean('1.5'))
@@ -239,6 +245,7 @@ def test_floatfield_3(self):
def test_decimalfield_1(self):
f = DecimalField(max_digits=4, decimal_places=2)
+ self.assertWidgetRendersTo(f, '<input id="id_f" step="0.01" type="number" name="f" maxlength="5" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertEqual(f.clean('1'), Decimal("1"))
@@ -284,6 +291,7 @@ def test_decimalfield_2(self):
def test_decimalfield_3(self):
f = DecimalField(max_digits=4, decimal_places=2, max_value=Decimal('1.5'), min_value=Decimal('0.5'))
+ self.assertWidgetRendersTo(f, '<input step="0.01" name="f" min="0.5" max="1.5" maxlength="5" type="number" id="id_f" />')
self.assertRaisesMessage(ValidationError, "'Ensure this value is less than or equal to 1.5.'", f.clean, '1.6')
self.assertRaisesMessage(ValidationError, "'Ensure this value is greater than or equal to 0.5.'", f.clean, '0.4')
self.assertEqual(f.clean('1.5'), Decimal("1.5"))
View
6 tests/regressiontests/forms/tests/forms.py
@@ -1740,7 +1740,7 @@ class Person(Form):
<option value="3">No</option>
</select></li>
<li><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" /></li>
-<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></li>""")
+<li class="required error"><ul class="errorlist"><li>This field is required.</li></ul><label for="id_age">Age:</label> <input type="number" name="age" id="id_age" /></li>""")
self.assertHTMLEqual(p.as_p(), """<ul class="errorlist"><li>This field is required.</li></ul>
<p class="required error"><label for="id_name">Name:</label> <input type="text" name="name" id="id_name" /></p>
@@ -1751,7 +1751,7 @@ class Person(Form):
</select></p>
<p><label for="id_email">Email:</label> <input type="email" name="email" id="id_email" /></p>
<ul class="errorlist"><li>This field is required.</li></ul>
-<p class="required error"><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p>""")
+<p class="required error"><label for="id_age">Age:</label> <input type="number" name="age" id="id_age" /></p>""")
self.assertHTMLEqual(p.as_table(), """<tr class="required error"><th><label for="id_name">Name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="name" id="id_name" /></td></tr>
<tr class="required"><th><label for="id_is_cool">Is cool:</label></th><td><select name="is_cool" id="id_is_cool">
@@ -1760,7 +1760,7 @@ class Person(Form):
<option value="3">No</option>
</select></td></tr>
<tr><th><label for="id_email">Email:</label></th><td><input type="email" name="email" id="id_email" /></td></tr>
-<tr class="required error"><th><label for="id_age">Age:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="age" id="id_age" /></td></tr>""")
+<tr class="required error"><th><label for="id_age">Age:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="number" name="age" id="id_age" /></td></tr>""")
def test_label_split_datetime_not_displayed(self):
class EventForm(Form):
View
62 tests/regressiontests/forms/tests/formsets.py
@@ -53,7 +53,7 @@ def test_basic_formset(self):
formset = ChoiceFormSet(auto_id=False, prefix='choices')
self.assertHTMLEqual(str(formset), """<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" /></td></tr>
-<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" /></td></tr>""")
+<tr><th>Votes:</th><td><input type="number" name="choices-0-votes" /></td></tr>""")
# On thing to note is that there needs to be a special value in the data. This
# value tells the FormSet how many forms were displayed so it can tell how
@@ -137,9 +137,9 @@ def test_formset_initial_data(self):
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
-<li>Votes: <input type="text" name="choices-1-votes" /></li>""")
+<li>Votes: <input type="number" name="choices-1-votes" /></li>""")
# Let's simulate what would happen if we submitted this form.
@@ -210,11 +210,11 @@ def test_displaying_more_than_one_blank_form(self):
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
-<li>Votes: <input type="text" name="choices-1-votes" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
-<li>Votes: <input type="text" name="choices-2-votes" /></li>""")
+<li>Votes: <input type="number" name="choices-2-votes" /></li>""")
# Since we displayed every form as blank, we will also accept them back as blank.
# This may seem a little strange, but later we will show how to require a minimum
@@ -301,19 +301,19 @@ def test_more_initial_data(self):
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
<li>Choice: <input type="text" name="choices-1-choice" /></li>
-<li>Votes: <input type="text" name="choices-1-votes" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
-<li>Votes: <input type="text" name="choices-2-votes" /></li>
+<li>Votes: <input type="number" name="choices-2-votes" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
-<li>Votes: <input type="text" name="choices-3-votes" /></li>""")
+<li>Votes: <input type="number" name="choices-3-votes" /></li>""")
# Make sure retrieving an empty form works, and it shows up in the form list
self.assertTrue(formset.empty_form.empty_permitted)
self.assertHTMLEqual(formset.empty_form.as_ul(), """<li>Choice: <input type="text" name="choices-__prefix__-choice" /></li>
-<li>Votes: <input type="text" name="choices-__prefix__-votes" /></li>""")
+<li>Votes: <input type="number" name="choices-__prefix__-votes" /></li>""")
def test_formset_with_deletion(self):
# FormSets with deletion ######################################################
@@ -331,13 +331,13 @@ def test_formset_with_deletion(self):
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
-<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" value="900" /></li>
<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
-<li>Votes: <input type="text" name="choices-2-votes" /></li>
+<li>Votes: <input type="number" name="choices-2-votes" /></li>
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>""")
# To delete something, we just need to set that form's special delete field to
@@ -428,14 +428,14 @@ def test_formsets_with_ordering(self):
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
-<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
+<li>Order: <input type="number" name="choices-0-ORDER" value="1" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
-<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
-<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" value="900" /></li>
+<li>Order: <input type="number" name="choices-1-ORDER" value="2" /></li>
<li>Choice: <input type="text" name="choices-2-choice" /></li>
-<li>Votes: <input type="text" name="choices-2-votes" /></li>
-<li>Order: <input type="text" name="choices-2-ORDER" /></li>""")
+<li>Votes: <input type="number" name="choices-2-votes" /></li>
+<li>Order: <input type="number" name="choices-2-ORDER" /></li>""")
data = {
'choices-TOTAL_FORMS': '3', # the number of forms rendered
@@ -539,20 +539,20 @@ def test_formset_with_ordering_and_deletion(self):
form_output.append(form.as_ul())
self.assertHTMLEqual('\n'.join(form_output), """<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>
-<li>Order: <input type="text" name="choices-0-ORDER" value="1" /></li>
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>
+<li>Order: <input type="number" name="choices-0-ORDER" value="1" /></li>
<li>Delete: <input type="checkbox" name="choices-0-DELETE" /></li>
<li>Choice: <input type="text" name="choices-1-choice" value="Fergie" /></li>
-<li>Votes: <input type="text" name="choices-1-votes" value="900" /></li>
-<li>Order: <input type="text" name="choices-1-ORDER" value="2" /></li>
+<li>Votes: <input type="number" name="choices-1-votes" value="900" /></li>
+<li>Order: <input type="number" name="choices-1-ORDER" value="2" /></li>
<li>Delete: <input type="checkbox" name="choices-1-DELETE" /></li>
<li>Choice: <input type="text" name="choices-2-choice" value="The Decemberists" /></li>
-<li>Votes: <input type="text" name="choices-2-votes" value="500" /></li>
-<li>Order: <input type="text" name="choices-2-ORDER" value="3" /></li>
+<li>Votes: <input type="number" name="choices-2-votes" value="500" /></li>
+<li>Order: <input type="number" name="choices-2-ORDER" value="3" /></li>
<li>Delete: <input type="checkbox" name="choices-2-DELETE" /></li>
<li>Choice: <input type="text" name="choices-3-choice" /></li>
-<li>Votes: <input type="text" name="choices-3-votes" /></li>
-<li>Order: <input type="text" name="choices-3-ORDER" /></li>
+<li>Votes: <input type="number" name="choices-3-votes" /></li>
+<li>Order: <input type="number" name="choices-3-ORDER" /></li>
<li>Delete: <input type="checkbox" name="choices-3-DELETE" /></li>""")
# Let's delete Fergie, and put The Decemberists ahead of Calexico.
@@ -898,19 +898,19 @@ def test_as_table(self):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertHTMLEqual(formset.as_table(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" />
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice" value="Calexico" /></td></tr>
-<tr><th>Votes:</th><td><input type="text" name="choices-0-votes" value="100" /></td></tr>""")
+<tr><th>Votes:</th><td><input type="number" name="choices-0-votes" value="100" /></td></tr>""")
def test_as_p(self):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertHTMLEqual(formset.as_p(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" />
<p>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></p>
-<p>Votes: <input type="text" name="choices-0-votes" value="100" /></p>""")
+<p>Votes: <input type="number" name="choices-0-votes" value="100" /></p>""")
def test_as_ul(self):
formset = ChoiceFormSet(data, auto_id=False, prefix='choices')
self.assertHTMLEqual(formset.as_ul(),"""<input type="hidden" name="choices-TOTAL_FORMS" value="1" /><input type="hidden" name="choices-INITIAL_FORMS" value="0" /><input type="hidden" name="choices-MAX_NUM_FORMS" value="0" />
<li>Choice: <input type="text" name="choices-0-choice" value="Calexico" /></li>
-<li>Votes: <input type="text" name="choices-0-votes" value="100" /></li>""")
+<li>Votes: <input type="number" name="choices-0-votes" value="100" /></li>""")
# Regression test for #11418 #################################################
View
1  tests/regressiontests/i18n/tests.py
@@ -662,6 +662,7 @@ def test_localized_input(self):
"""
Tests if form input is correctly localized
"""
+ self.maxDiff = 1200
settings.USE_L10N = True
with translation.override('de-at', deactivate=True):
form6 = CompanyForm({
Something went wrong with that request. Please try again.