Moved has_changed logic from widget to form field

Refs #16612. Thanks Aymeric Augustin for the suggestion.
This commit is contained in:
Claude Paroz 2013-01-25 20:50:46 +01:00
parent ce27fb198d
commit ebb504db69
14 changed files with 230 additions and 251 deletions

View File

@ -213,17 +213,6 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
if value: if value:
return value.split(',') return value.split(',')
def _has_changed(self, initial, data):
if initial is None:
initial = []
if data is None:
data = []
if len(initial) != len(data):
return True
for pk1, pk2 in zip(initial, data):
if force_text(pk1) != force_text(pk2):
return True
return False
class RelatedFieldWidgetWrapper(forms.Widget): class RelatedFieldWidgetWrapper(forms.Widget):
""" """
@ -279,9 +268,6 @@ class RelatedFieldWidgetWrapper(forms.Widget):
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
return self.widget.value_from_datadict(data, files, name) return self.widget.value_from_datadict(data, files, name)
def _has_changed(self, initial, data):
return self.widget._has_changed(initial, data)
def id_for_label(self, id_): def id_for_label(self, id_):
return self.widget.id_for_label(id_) return self.widget.id_for_label(id_)

View File

@ -7,7 +7,7 @@ from django.utils import six
from django.utils import translation from django.utils import translation
from django.contrib.gis.gdal import OGRException from django.contrib.gis.gdal import OGRException
from django.contrib.gis.geos import GEOSGeometry, GEOSException, fromstr from django.contrib.gis.geos import GEOSGeometry, GEOSException
# Creating a template context that contains Django settings # Creating a template context that contains Django settings
# values needed by admin map templates. # values needed by admin map templates.
@ -117,25 +117,3 @@ class OpenLayersWidget(Textarea):
raise TypeError raise TypeError
map_options[js_name] = value map_options[js_name] = value
return map_options return map_options
def _has_changed(self, initial, data):
""" Compare geographic value of data with its initial value. """
# Ensure we are dealing with a geographic object
if isinstance(initial, six.string_types):
try:
initial = GEOSGeometry(initial)
except (GEOSException, ValueError):
initial = None
# Only do a geographic comparison if both values are available
if initial and data:
data = fromstr(data)
data.transform(initial.srid)
# If the initial value was not added by the browser, the geometry
# provided may be slightly different, the first time it is saved.
# The comparison is done with a very low tolerance.
return not initial.equals_exact(data, tolerance=0.000001)
else:
# Check for change of state of existence
return bool(initial) != bool(data)

View File

@ -1,11 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django import forms from django import forms
from django.utils import six
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
# While this couples the geographic forms to the GEOS library, # While this couples the geographic forms to the GEOS library,
# it decouples from database (by not importing SpatialBackend). # it decouples from database (by not importing SpatialBackend).
from django.contrib.gis.geos import GEOSException, GEOSGeometry from django.contrib.gis.geos import GEOSException, GEOSGeometry, fromstr
class GeometryField(forms.Field): class GeometryField(forms.Field):
""" """
@ -73,3 +75,25 @@ class GeometryField(forms.Field):
raise forms.ValidationError(self.error_messages['transform_error']) raise forms.ValidationError(self.error_messages['transform_error'])
return geom return geom
def _has_changed(self, initial, data):
""" Compare geographic value of data with its initial value. """
# Ensure we are dealing with a geographic object
if isinstance(initial, six.string_types):
try:
initial = GEOSGeometry(initial)
except (GEOSException, ValueError):
initial = None
# Only do a geographic comparison if both values are available
if initial and data:
data = fromstr(data)
data.transform(initial.srid)
# If the initial value was not added by the browser, the geometry
# provided may be slightly different, the first time it is saved.
# The comparison is done with a very low tolerance.
return not initial.equals_exact(data, tolerance=0.000001)
else:
# Check for change of state of existence
return bool(initial) != bool(data)

View File

@ -38,7 +38,7 @@ class GeoAdminTest(TestCase):
""" Check that changes are accurately noticed by OpenLayersWidget. """ """ Check that changes are accurately noticed by OpenLayersWidget. """
geoadmin = admin.site._registry[City] geoadmin = admin.site._registry[City]
form = geoadmin.get_changelist_form(None)() form = geoadmin.get_changelist_form(None)()
has_changed = form.fields['point'].widget._has_changed has_changed = form.fields['point']._has_changed
initial = Point(13.4197458572965953, 52.5194108501149799, srid=4326) initial = Point(13.4197458572965953, 52.5194108501149799, srid=4326)
data_same = "SRID=3857;POINT(1493879.2754093995 6894592.019687599)" data_same = "SRID=3857;POINT(1493879.2754093995 6894592.019687599)"

View File

@ -135,11 +135,3 @@ class SelectDateWidget(Widget):
s = Select(choices=choices) s = Select(choices=choices)
select_html = s.render(field % name, val, local_attrs) select_html = s.render(field % name, val, local_attrs)
return select_html return select_html
def _has_changed(self, initial, data):
try:
input_format = get_format('DATE_INPUT_FORMATS')[0]
data = datetime_safe.datetime.strptime(data, input_format).date()
except (TypeError, ValueError):
pass
return super(SelectDateWidget, self)._has_changed(initial, data)

View File

@ -175,6 +175,25 @@ class Field(object):
""" """
return {} return {}
def _has_changed(self, initial, data):
"""
Return True if data differs from initial.
"""
# For purposes of seeing whether something has changed, None is
# the same as an empty string, if the data or inital value we get
# is None, replace it w/ ''.
if data is None:
data_value = ''
else:
data_value = data
if initial is None:
initial_value = ''
else:
initial_value = initial
if force_text(initial_value) != force_text(data_value):
return True
return False
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
result = copy.copy(self) result = copy.copy(self)
memo[id(self)] = result memo[id(self)] = result
@ -348,6 +367,13 @@ class BaseTemporalField(Field):
def strptime(self, value, format): def strptime(self, value, format):
raise NotImplementedError('Subclasses must define this method.') raise NotImplementedError('Subclasses must define this method.')
def _has_changed(self, initial, data):
try:
data = self.to_python(data)
except ValidationError:
return True
return self.to_python(initial) != data
class DateField(BaseTemporalField): class DateField(BaseTemporalField):
widget = DateInput widget = DateInput
input_formats = formats.get_format_lazy('DATE_INPUT_FORMATS') input_formats = formats.get_format_lazy('DATE_INPUT_FORMATS')
@ -371,6 +397,7 @@ class DateField(BaseTemporalField):
def strptime(self, value, format): def strptime(self, value, format):
return datetime.datetime.strptime(value, format).date() return datetime.datetime.strptime(value, format).date()
class TimeField(BaseTemporalField): class TimeField(BaseTemporalField):
widget = TimeInput widget = TimeInput
input_formats = formats.get_format_lazy('TIME_INPUT_FORMATS') input_formats = formats.get_format_lazy('TIME_INPUT_FORMATS')
@ -529,6 +556,12 @@ class FileField(Field):
return initial return initial
return data return data
def _has_changed(self, initial, data):
if data is None:
return False
return True
class ImageField(FileField): class ImageField(FileField):
default_error_messages = { default_error_messages = {
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
@ -618,6 +651,7 @@ class URLField(CharField):
value = urlunsplit(url_fields) value = urlunsplit(url_fields)
return value return value
class BooleanField(Field): class BooleanField(Field):
widget = CheckboxInput widget = CheckboxInput
@ -636,6 +670,15 @@ class BooleanField(Field):
raise ValidationError(self.error_messages['required']) raise ValidationError(self.error_messages['required'])
return value return value
def _has_changed(self, initial, data):
# Sometimes data or initial could be None or '' which should be the
# same thing as False.
if initial == 'False':
# show_hidden_initial may have transformed False to 'False'
initial = False
return bool(initial) != bool(data)
class NullBooleanField(BooleanField): class NullBooleanField(BooleanField):
""" """
A field whose valid values are None, True and False. Invalid values are A field whose valid values are None, True and False. Invalid values are
@ -660,6 +703,15 @@ class NullBooleanField(BooleanField):
def validate(self, value): def validate(self, value):
pass pass
def _has_changed(self, initial, data):
# None (unknown) and False (No) are not the same
if initial is not None:
initial = bool(initial)
if data is not None:
data = bool(data)
return initial != data
class ChoiceField(Field): class ChoiceField(Field):
widget = Select widget = Select
default_error_messages = { default_error_messages = {
@ -739,6 +791,7 @@ class TypedChoiceField(ChoiceField):
def validate(self, value): def validate(self, value):
pass pass
class MultipleChoiceField(ChoiceField): class MultipleChoiceField(ChoiceField):
hidden_widget = MultipleHiddenInput hidden_widget = MultipleHiddenInput
widget = SelectMultiple widget = SelectMultiple
@ -765,6 +818,18 @@ class MultipleChoiceField(ChoiceField):
if not self.valid_value(val): if not self.valid_value(val):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
def _has_changed(self, initial, data):
if initial is None:
initial = []
if data is None:
data = []
if len(initial) != len(data):
return True
initial_set = set([force_text(value) for value in initial])
data_set = set([force_text(value) for value in data])
return data_set != initial_set
class TypedMultipleChoiceField(MultipleChoiceField): class TypedMultipleChoiceField(MultipleChoiceField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.coerce = kwargs.pop('coerce', lambda val: val) self.coerce = kwargs.pop('coerce', lambda val: val)
@ -899,6 +964,18 @@ class MultiValueField(Field):
""" """
raise NotImplementedError('Subclasses must implement this method.') raise NotImplementedError('Subclasses must implement this method.')
def _has_changed(self, initial, data):
if initial is None:
initial = ['' for x in range(0, len(data))]
else:
if not isinstance(initial, list):
initial = self.widget.decompress(initial)
for field, initial, data in zip(self.fields, initial, data):
if field._has_changed(initial, data):
return True
return False
class FilePathField(ChoiceField): class FilePathField(ChoiceField):
def __init__(self, path, match=None, recursive=False, allow_files=True, def __init__(self, path, match=None, recursive=False, allow_files=True,
allow_folders=False, required=True, widget=None, label=None, allow_folders=False, required=True, widget=None, label=None,

View File

@ -341,7 +341,13 @@ class BaseForm(object):
hidden_widget = field.hidden_widget() hidden_widget = field.hidden_widget()
initial_value = hidden_widget.value_from_datadict( initial_value = hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name) self.data, self.files, initial_prefixed_name)
if field.widget._has_changed(initial_value, data_value): if hasattr(field.widget, '_has_changed'):
warnings.warn("The _has_changed method on widgets is deprecated,"
" define it at field level instead.",
PendingDeprecationWarning, stacklevel=2)
if field.widget._has_changed(initial_value, data_value):
self._changed_data.append(name)
elif field._has_changed(initial_value, data_value):
self._changed_data.append(name) self._changed_data.append(name)
return self._changed_data return self._changed_data
changed_data = property(_get_changed_data) changed_data = property(_get_changed_data)

View File

@ -858,15 +858,12 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
# Fields ##################################################################### # Fields #####################################################################
class InlineForeignKeyHiddenInput(HiddenInput):
def _has_changed(self, initial, data):
return False
class InlineForeignKeyField(Field): class InlineForeignKeyField(Field):
""" """
A basic integer field that deals with validating the given value to a A basic integer field that deals with validating the given value to a
given parent instance in an inline. given parent instance in an inline.
""" """
widget = HiddenInput
default_error_messages = { default_error_messages = {
'invalid_choice': _('The inline foreign key did not match the parent instance primary key.'), 'invalid_choice': _('The inline foreign key did not match the parent instance primary key.'),
} }
@ -881,7 +878,6 @@ class InlineForeignKeyField(Field):
else: else:
kwargs["initial"] = self.parent_instance.pk kwargs["initial"] = self.parent_instance.pk
kwargs["required"] = False kwargs["required"] = False
kwargs["widget"] = InlineForeignKeyHiddenInput
super(InlineForeignKeyField, self).__init__(*args, **kwargs) super(InlineForeignKeyField, self).__init__(*args, **kwargs)
def clean(self, value): def clean(self, value):
@ -899,6 +895,9 @@ class InlineForeignKeyField(Field):
raise ValidationError(self.error_messages['invalid_choice']) raise ValidationError(self.error_messages['invalid_choice'])
return self.parent_instance return self.parent_instance
def _has_changed(self, initial, data):
return False
class ModelChoiceIterator(object): class ModelChoiceIterator(object):
def __init__(self, field): def __init__(self, field):
self.field = field self.field = field

View File

@ -208,25 +208,6 @@ class Widget(six.with_metaclass(MediaDefiningClass)):
""" """
return data.get(name, None) return data.get(name, None)
def _has_changed(self, initial, data):
"""
Return True if data differs from initial.
"""
# For purposes of seeing whether something has changed, None is
# the same as an empty string, if the data or inital value we get
# is None, replace it w/ ''.
if data is None:
data_value = ''
else:
data_value = data
if initial is None:
initial_value = ''
else:
initial_value = initial
if force_text(initial_value) != force_text(data_value):
return True
return False
def id_for_label(self, id_): def id_for_label(self, id_):
""" """
Returns the HTML ID attribute of this Widget for use by a <label>, Returns the HTML ID attribute of this Widget for use by a <label>,
@ -325,10 +306,6 @@ class FileInput(Input):
"File widgets take data from FILES, not POST" "File widgets take data from FILES, not POST"
return files.get(name, None) return files.get(name, None)
def _has_changed(self, initial, data):
if data is None:
return False
return True
FILE_INPUT_CONTRADICTION = object() FILE_INPUT_CONTRADICTION = object()
@ -426,17 +403,6 @@ class DateInput(TextInput):
return value.strftime(self.format) return value.strftime(self.format)
return value return value
def _has_changed(self, initial, data):
# If our field has show_hidden_initial=True, initial will be a string
# formatted by HiddenInput using formats.localize_input, which is not
# necessarily the format used for this widget. Attempt to convert it.
try:
input_format = formats.get_format('DATE_INPUT_FORMATS')[0]
initial = datetime.datetime.strptime(initial, input_format).date()
except (TypeError, ValueError):
pass
return super(DateInput, self)._has_changed(self._format_value(initial), data)
class DateTimeInput(TextInput): class DateTimeInput(TextInput):
def __init__(self, attrs=None, format=None): def __init__(self, attrs=None, format=None):
@ -456,17 +422,6 @@ class DateTimeInput(TextInput):
return value.strftime(self.format) return value.strftime(self.format)
return value return value
def _has_changed(self, initial, data):
# If our field has show_hidden_initial=True, initial will be a string
# formatted by HiddenInput using formats.localize_input, which is not
# necessarily the format used for this widget. Attempt to convert it.
try:
input_format = formats.get_format('DATETIME_INPUT_FORMATS')[0]
initial = datetime.datetime.strptime(initial, input_format)
except (TypeError, ValueError):
pass
return super(DateTimeInput, self)._has_changed(self._format_value(initial), data)
class TimeInput(TextInput): class TimeInput(TextInput):
def __init__(self, attrs=None, format=None): def __init__(self, attrs=None, format=None):
@ -485,17 +440,6 @@ class TimeInput(TextInput):
return value.strftime(self.format) return value.strftime(self.format)
return value return value
def _has_changed(self, initial, data):
# If our field has show_hidden_initial=True, initial will be a string
# formatted by HiddenInput using formats.localize_input, which is not
# necessarily the format used for this widget. Attempt to convert it.
try:
input_format = formats.get_format('TIME_INPUT_FORMATS')[0]
initial = datetime.datetime.strptime(initial, input_format).time()
except (TypeError, ValueError):
pass
return super(TimeInput, self)._has_changed(self._format_value(initial), data)
# Defined at module level so that CheckboxInput is picklable (#17976) # Defined at module level so that CheckboxInput is picklable (#17976)
def boolean_check(v): def boolean_check(v):
@ -530,13 +474,6 @@ class CheckboxInput(Widget):
value = values.get(value.lower(), value) value = values.get(value.lower(), value)
return bool(value) return bool(value)
def _has_changed(self, initial, data):
# Sometimes data or initial could be None or '' which should be the
# same thing as False.
if initial == 'False':
# show_hidden_initial may have transformed False to 'False'
initial = False
return bool(initial) != bool(data)
class Select(Widget): class Select(Widget):
allow_multiple_selected = False allow_multiple_selected = False
@ -612,14 +549,6 @@ class NullBooleanSelect(Select):
'False': False, 'False': False,
False: False}.get(value, None) False: False}.get(value, None)
def _has_changed(self, initial, data):
# For a NullBooleanSelect, None (unknown) and False (No)
# are not the same
if initial is not None:
initial = bool(initial)
if data is not None:
data = bool(data)
return initial != data
class SelectMultiple(Select): class SelectMultiple(Select):
allow_multiple_selected = True allow_multiple_selected = True
@ -639,16 +568,6 @@ class SelectMultiple(Select):
return data.getlist(name) return data.getlist(name)
return data.get(name, None) return data.get(name, None)
def _has_changed(self, initial, data):
if initial is None:
initial = []
if data is None:
data = []
if len(initial) != len(data):
return True
initial_set = set([force_text(value) for value in initial])
data_set = set([force_text(value) for value in data])
return data_set != initial_set
@python_2_unicode_compatible @python_2_unicode_compatible
class RadioInput(SubWidget): class RadioInput(SubWidget):
@ -844,17 +763,6 @@ class MultiWidget(Widget):
def value_from_datadict(self, data, files, name): def value_from_datadict(self, data, files, name):
return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)] return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)]
def _has_changed(self, initial, data):
if initial is None:
initial = ['' for x in range(0, len(data))]
else:
if not isinstance(initial, list):
initial = self.decompress(initial)
for widget, initial, data in zip(self.widgets, initial, data):
if widget._has_changed(initial, data):
return True
return False
def format_output(self, rendered_widgets): def format_output(self, rendered_widgets):
""" """
Given a list of rendered widgets (as strings), returns a Unicode string Given a list of rendered widgets (as strings), returns a Unicode string

View File

@ -67,3 +67,9 @@ If you're relying on this feature, you should add
``'django.middleware.common.BrokenLinkEmailsMiddleware'`` to your ``'django.middleware.common.BrokenLinkEmailsMiddleware'`` to your
:setting:`MIDDLEWARE_CLASSES` setting and remove ``SEND_BROKEN_LINK_EMAILS`` :setting:`MIDDLEWARE_CLASSES` setting and remove ``SEND_BROKEN_LINK_EMAILS``
from your settings. from your settings.
``_has_changed`` method on widgets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you defined your own form widgets and defined the ``_has_changed`` method
on a widget, you should now define this method on the form field itself.

View File

@ -425,13 +425,6 @@ class ManyToManyRawIdWidgetTest(DjangoTestCase):
'<input type="text" name="test" value="%(m1pk)s" class="vManyToManyRawIdAdminField" /><a href="/widget_admin/admin_widgets/member/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % dict(admin_static_prefix(), m1pk=m1.pk) '<input type="text" name="test" value="%(m1pk)s" class="vManyToManyRawIdAdminField" /><a href="/widget_admin/admin_widgets/member/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%(ADMIN_STATIC_PREFIX)simg/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % dict(admin_static_prefix(), m1pk=m1.pk)
) )
self.assertEqual(w._has_changed(None, None), False)
self.assertEqual(w._has_changed([], None), False)
self.assertEqual(w._has_changed(None, ['1']), True)
self.assertEqual(w._has_changed([1, 2], ['1', '2']), False)
self.assertEqual(w._has_changed([1, 2], ['1']), True)
self.assertEqual(w._has_changed([1, 2], ['1', '3']), True)
def test_m2m_related_model_not_in_admin(self): def test_m2m_related_model_not_in_admin(self):
# M2M relationship with model not registered with admin site. Raw ID # M2M relationship with model not registered with admin site. Raw ID
# widget should have no magnifying glass link. See #16542 # widget should have no magnifying glass link. See #16542

View File

@ -428,6 +428,23 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin):
# If insufficient data is provided, None is substituted # If insufficient data is provided, None is substituted
self.assertFormErrors(['This field is required.'], f.clean, ['some text',['JP']]) self.assertFormErrors(['This field is required.'], f.clean, ['some text',['JP']])
# test with no initial data
self.assertTrue(f._has_changed(None, ['some text', ['J','P'], ['2007-04-25','6:24:00']]))
# test when the data is the same as initial
self.assertFalse(f._has_changed('some text,JP,2007-04-25 06:24:00',
['some text', ['J','P'], ['2007-04-25','6:24:00']]))
# test when the first widget's data has changed
self.assertTrue(f._has_changed('some text,JP,2007-04-25 06:24:00',
['other text', ['J','P'], ['2007-04-25','6:24:00']]))
# test when the last widget's data has changed. this ensures that it is not
# short circuiting while testing the widgets.
self.assertTrue(f._has_changed('some text,JP,2007-04-25 06:24:00',
['some text', ['J','P'], ['2009-04-25','11:44:00']]))
class ComplexFieldForm(Form): class ComplexFieldForm(Form):
field1 = ComplexField(widget=w) field1 = ComplexField(widget=w)
@ -725,8 +742,8 @@ class FormsExtraL10NTestCase(TestCase):
def test_l10n_date_changed(self): def test_l10n_date_changed(self):
""" """
Ensure that SelectDateWidget._has_changed() works correctly with a Ensure that DateField._has_changed() with SelectDateWidget works
localized date format. correctly with a localized date format.
Refs #17165. Refs #17165.
""" """
# With Field.show_hidden_initial=False ----------------------- # With Field.show_hidden_initial=False -----------------------

View File

@ -35,6 +35,7 @@ from decimal import Decimal
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import * from django.forms import *
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.utils import formats
from django.utils import six from django.utils import six
from django.utils._os import upath from django.utils._os import upath
@ -362,6 +363,13 @@ class FieldsTests(SimpleTestCase):
f = DateField() f = DateField()
self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, 'a\x00b') self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, 'a\x00b')
def test_datefield_changed(self):
format = '%d/%m/%Y'
f = DateField(input_formats=[format])
d = datetime.date(2007, 9, 17)
self.assertFalse(f._has_changed(d, '17/09/2007'))
self.assertFalse(f._has_changed(d.strftime(format), '17/09/2007'))
# TimeField ################################################################### # TimeField ###################################################################
def test_timefield_1(self): def test_timefield_1(self):
@ -388,6 +396,18 @@ class FieldsTests(SimpleTestCase):
self.assertEqual(datetime.time(14, 25, 59), f.clean(' 14:25:59 ')) self.assertEqual(datetime.time(14, 25, 59), f.clean(' 14:25:59 '))
self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ' ') self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ' ')
def test_timefield_changed(self):
t1 = datetime.time(12, 51, 34, 482548)
t2 = datetime.time(12, 51)
format = '%H:%M'
f = TimeField(input_formats=[format])
self.assertTrue(f._has_changed(t1, '12:51'))
self.assertFalse(f._has_changed(t2, '12:51'))
format = '%I:%M %p'
f = TimeField(input_formats=[format])
self.assertFalse(f._has_changed(t2.strftime(format), '12:51 PM'))
# DateTimeField ############################################################### # DateTimeField ###############################################################
def test_datetimefield_1(self): def test_datetimefield_1(self):
@ -446,6 +466,15 @@ class FieldsTests(SimpleTestCase):
def test_datetimefield_5(self): def test_datetimefield_5(self):
f = DateTimeField(input_formats=['%Y.%m.%d %H:%M:%S.%f']) f = DateTimeField(input_formats=['%Y.%m.%d %H:%M:%S.%f'])
self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('2006.10.25 14:30:45.0002')) self.assertEqual(datetime.datetime(2006, 10, 25, 14, 30, 45, 200), f.clean('2006.10.25 14:30:45.0002'))
def test_datetimefield_changed(self):
format = '%Y %m %d %I:%M %p'
f = DateTimeField(input_formats=[format])
d = datetime.datetime(2006, 9, 17, 14, 30, 0)
self.assertFalse(f._has_changed(d, '2006 09 17 2:30 PM'))
# Initial value may be a string from a hidden input
self.assertFalse(f._has_changed(d.strftime(format), '2006 09 17 2:30 PM'))
# RegexField ################################################################## # RegexField ##################################################################
def test_regexfield_1(self): def test_regexfield_1(self):
@ -566,6 +595,29 @@ class FieldsTests(SimpleTestCase):
self.assertEqual(SimpleUploadedFile, self.assertEqual(SimpleUploadedFile,
type(f.clean(SimpleUploadedFile('name', b'')))) type(f.clean(SimpleUploadedFile('name', b''))))
def test_filefield_changed(self):
'''
Test for the behavior of _has_changed for FileField. The value of data will
more than likely come from request.FILES. The value of initial data will
likely be a filename stored in the database. Since its value is of no use to
a FileField it is ignored.
'''
f = FileField()
# No file was uploaded and no initial data.
self.assertFalse(f._has_changed('', None))
# A file was uploaded and no initial data.
self.assertTrue(f._has_changed('', {'filename': 'resume.txt', 'content': 'My resume'}))
# A file was not uploaded, but there is initial data
self.assertFalse(f._has_changed('resume.txt', None))
# A file was uploaded and there is initial data (file identity is not dealt
# with here)
self.assertTrue(f._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'}))
# URLField ################################################################## # URLField ##################################################################
def test_urlfield_1(self): def test_urlfield_1(self):
@ -709,6 +761,18 @@ class FieldsTests(SimpleTestCase):
def test_boolean_picklable(self): def test_boolean_picklable(self):
self.assertIsInstance(pickle.loads(pickle.dumps(BooleanField())), BooleanField) self.assertIsInstance(pickle.loads(pickle.dumps(BooleanField())), BooleanField)
def test_booleanfield_changed(self):
f = BooleanField()
self.assertFalse(f._has_changed(None, None))
self.assertFalse(f._has_changed(None, ''))
self.assertFalse(f._has_changed('', None))
self.assertFalse(f._has_changed('', ''))
self.assertTrue(f._has_changed(False, 'on'))
self.assertFalse(f._has_changed(True, 'on'))
self.assertTrue(f._has_changed(True, ''))
# Initial value may have mutated to a string due to show_hidden_initial (#19537)
self.assertTrue(f._has_changed('False', 'on'))
# ChoiceField ################################################################# # ChoiceField #################################################################
def test_choicefield_1(self): def test_choicefield_1(self):
@ -825,6 +889,16 @@ class FieldsTests(SimpleTestCase):
self.assertEqual(False, f.cleaned_data['nullbool1']) self.assertEqual(False, f.cleaned_data['nullbool1'])
self.assertEqual(None, f.cleaned_data['nullbool2']) self.assertEqual(None, f.cleaned_data['nullbool2'])
def test_nullbooleanfield_changed(self):
f = NullBooleanField()
self.assertTrue(f._has_changed(False, None))
self.assertTrue(f._has_changed(None, False))
self.assertFalse(f._has_changed(None, None))
self.assertFalse(f._has_changed(False, False))
self.assertTrue(f._has_changed(True, False))
self.assertTrue(f._has_changed(True, None))
self.assertTrue(f._has_changed(True, False))
# MultipleChoiceField ######################################################### # MultipleChoiceField #########################################################
def test_multiplechoicefield_1(self): def test_multiplechoicefield_1(self):
@ -866,6 +940,16 @@ class FieldsTests(SimpleTestCase):
self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['6']) self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['6'])
self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['1','6']) self.assertRaisesMessage(ValidationError, "'Select a valid choice. 6 is not one of the available choices.'", f.clean, ['1','6'])
def test_multiplechoicefield_changed(self):
f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two'), ('3', 'Three')])
self.assertFalse(f._has_changed(None, None))
self.assertFalse(f._has_changed([], None))
self.assertTrue(f._has_changed(None, ['1']))
self.assertFalse(f._has_changed([1, 2], ['1', '2']))
self.assertFalse(f._has_changed([2, 1], ['1', '2']))
self.assertTrue(f._has_changed([1, 2], ['1']))
self.assertTrue(f._has_changed([1, 2], ['1', '3']))
# TypedMultipleChoiceField ############################################################ # TypedMultipleChoiceField ############################################################
# TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types # TypedMultipleChoiceField is just like MultipleChoiceField, except that coerced types
# will be returned: # will be returned:
@ -1048,3 +1132,9 @@ class FieldsTests(SimpleTestCase):
self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10', '']) self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10', ''])
self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10']) self.assertRaisesMessage(ValidationError, "'Enter a valid time.'", f.clean, ['2006-01-10'])
self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, ['', '07:30']) self.assertRaisesMessage(ValidationError, "'Enter a valid date.'", f.clean, ['', '07:30'])
def test_splitdatetimefield_changed(self):
f = SplitDateTimeField(input_date_formats=['%d/%m/%Y'])
self.assertTrue(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00']))
self.assertFalse(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40']))
self.assertTrue(f._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41']))

View File

@ -148,25 +148,6 @@ class FormsWidgetTestCase(TestCase):
self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '<input type="file" class="fun" name="email" />') self.assertHTMLEqual(w.render('email', 'ŠĐĆŽćžšđ', attrs={'class': 'fun'}), '<input type="file" class="fun" name="email" />')
# Test for the behavior of _has_changed for FileInput. The value of data will
# more than likely come from request.FILES. The value of initial data will
# likely be a filename stored in the database. Since its value is of no use to
# a FileInput it is ignored.
w = FileInput()
# No file was uploaded and no initial data.
self.assertFalse(w._has_changed('', None))
# A file was uploaded and no initial data.
self.assertTrue(w._has_changed('', {'filename': 'resume.txt', 'content': 'My resume'}))
# A file was not uploaded, but there is initial data
self.assertFalse(w._has_changed('resume.txt', None))
# A file was uploaded and there is initial data (file identity is not dealt
# with here)
self.assertTrue(w._has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'}))
def test_textarea(self): def test_textarea(self):
w = Textarea() w = Textarea()
self.assertHTMLEqual(w.render('msg', ''), '<textarea rows="10" cols="40" name="msg"></textarea>') self.assertHTMLEqual(w.render('msg', ''), '<textarea rows="10" cols="40" name="msg"></textarea>')
@ -233,16 +214,6 @@ class FormsWidgetTestCase(TestCase):
self.assertIsInstance(value, bool) self.assertIsInstance(value, bool)
self.assertTrue(value) self.assertTrue(value)
self.assertFalse(w._has_changed(None, None))
self.assertFalse(w._has_changed(None, ''))
self.assertFalse(w._has_changed('', None))
self.assertFalse(w._has_changed('', ''))
self.assertTrue(w._has_changed(False, 'on'))
self.assertFalse(w._has_changed(True, 'on'))
self.assertTrue(w._has_changed(True, ''))
# Initial value may have mutated to a string due to show_hidden_initial (#19537)
self.assertTrue(w._has_changed('False', 'on'))
def test_select(self): def test_select(self):
w = Select() w = Select()
self.assertHTMLEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select name="beatle"> self.assertHTMLEqual(w.render('beatle', 'J', choices=(('J', 'John'), ('P', 'Paul'), ('G', 'George'), ('R', 'Ringo'))), """<select name="beatle">
@ -415,13 +386,6 @@ class FormsWidgetTestCase(TestCase):
<option value="2">Yes</option> <option value="2">Yes</option>
<option value="3" selected="selected">No</option> <option value="3" selected="selected">No</option>
</select>""") </select>""")
self.assertTrue(w._has_changed(False, None))
self.assertTrue(w._has_changed(None, False))
self.assertFalse(w._has_changed(None, None))
self.assertFalse(w._has_changed(False, False))
self.assertTrue(w._has_changed(True, False))
self.assertTrue(w._has_changed(True, None))
self.assertTrue(w._has_changed(True, False))
def test_selectmultiple(self): def test_selectmultiple(self):
w = SelectMultiple() w = SelectMultiple()
@ -535,14 +499,6 @@ class FormsWidgetTestCase(TestCase):
# Unicode choices are correctly rendered as HTML # Unicode choices are correctly rendered as HTML
self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>') self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '<select multiple="multiple" name="nums">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" selected="selected">\u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</option>\n<option value="\u0107\u017e\u0161\u0111">abc\u0107\u017e\u0161\u0111</option>\n</select>')
# Test the usage of _has_changed
self.assertFalse(w._has_changed(None, None))
self.assertFalse(w._has_changed([], None))
self.assertTrue(w._has_changed(None, ['1']))
self.assertFalse(w._has_changed([1, 2], ['1', '2']))
self.assertTrue(w._has_changed([1, 2], ['1']))
self.assertTrue(w._has_changed([1, 2], ['1', '3']))
# Choices can be nested one level in order to create HTML optgroups: # Choices can be nested one level in order to create HTML optgroups:
w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2')))) w.choices = (('outer1', 'Outer 1'), ('Group "1"', (('inner1', 'Inner 1'), ('inner2', 'Inner 2'))))
self.assertHTMLEqual(w.render('nestchoice', None), """<select multiple="multiple" name="nestchoice"> self.assertHTMLEqual(w.render('nestchoice', None), """<select multiple="multiple" name="nestchoice">
@ -844,15 +800,6 @@ beatle J R Ringo False""")
<li><label><input type="checkbox" name="escape" value="good" /> you &gt; me</label></li> <li><label><input type="checkbox" name="escape" value="good" /> you &gt; me</label></li>
</ul>""") </ul>""")
# Test the usage of _has_changed
self.assertFalse(w._has_changed(None, None))
self.assertFalse(w._has_changed([], None))
self.assertTrue(w._has_changed(None, ['1']))
self.assertFalse(w._has_changed([1, 2], ['1', '2']))
self.assertTrue(w._has_changed([1, 2], ['1']))
self.assertTrue(w._has_changed([1, 2], ['1', '3']))
self.assertFalse(w._has_changed([2, 1], ['1', '2']))
# Unicode choices are correctly rendered as HTML # Unicode choices are correctly rendered as HTML
self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>') self.assertHTMLEqual(w.render('nums', ['ŠĐĆŽćžšđ'], choices=[('ŠĐĆŽćžšđ', 'ŠĐabcĆŽćžšđ'), ('ćžšđ', 'abcćžšđ')]), '<ul>\n<li><label><input type="checkbox" name="nums" value="1" /> 1</label></li>\n<li><label><input type="checkbox" name="nums" value="2" /> 2</label></li>\n<li><label><input type="checkbox" name="nums" value="3" /> 3</label></li>\n<li><label><input checked="checked" type="checkbox" name="nums" value="\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111" /> \u0160\u0110abc\u0106\u017d\u0107\u017e\u0161\u0111</label></li>\n<li><label><input type="checkbox" name="nums" value="\u0107\u017e\u0161\u0111" /> abc\u0107\u017e\u0161\u0111</label></li>\n</ul>')
@ -886,21 +833,6 @@ beatle J R Ringo False""")
w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})), attrs={'id': 'bar'}) w = MyMultiWidget(widgets=(TextInput(attrs={'class': 'big'}), TextInput(attrs={'class': 'small'})), attrs={'id': 'bar'})
self.assertHTMLEqual(w.render('name', ['john', 'lennon']), '<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />') self.assertHTMLEqual(w.render('name', ['john', 'lennon']), '<input id="bar_0" type="text" class="big" value="john" name="name_0" /><br /><input id="bar_1" type="text" class="small" value="lennon" name="name_1" />')
w = MyMultiWidget(widgets=(TextInput(), TextInput()))
# test with no initial data
self.assertTrue(w._has_changed(None, ['john', 'lennon']))
# test when the data is the same as initial
self.assertFalse(w._has_changed('john__lennon', ['john', 'lennon']))
# test when the first widget's data has changed
self.assertTrue(w._has_changed('john__lennon', ['alfred', 'lennon']))
# test when the last widget's data has changed. this ensures that it is not
# short circuiting while testing the widgets.
self.assertTrue(w._has_changed('john__lennon', ['john', 'denver']))
def test_splitdatetime(self): def test_splitdatetime(self):
w = SplitDateTimeWidget() w = SplitDateTimeWidget()
self.assertHTMLEqual(w.render('date', ''), '<input type="text" name="date_0" /><input type="text" name="date_1" />') self.assertHTMLEqual(w.render('date', ''), '<input type="text" name="date_0" /><input type="text" name="date_1" />')
@ -916,10 +848,6 @@ beatle J R Ringo False""")
w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M') w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M')
self.assertHTMLEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), '<input type="text" name="date_0" value="10/01/2006" /><input type="text" name="date_1" value="07:30" />') self.assertHTMLEqual(w.render('date', datetime.datetime(2006, 1, 10, 7, 30)), '<input type="text" name="date_0" value="10/01/2006" /><input type="text" name="date_1" value="07:30" />')
self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['2008-05-06', '12:40:00']))
self.assertFalse(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:40']))
self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06/05/2008', '12:41']))
def test_datetimeinput(self): def test_datetimeinput(self):
w = DateTimeInput() w = DateTimeInput()
self.assertHTMLEqual(w.render('date', None), '<input type="text" name="date" />') self.assertHTMLEqual(w.render('date', None), '<input type="text" name="date" />')
@ -934,13 +862,6 @@ beatle J R Ringo False""")
# Use 'format' to change the way a value is displayed. # Use 'format' to change the way a value is displayed.
w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'}) w = DateTimeInput(format='%d/%m/%Y %H:%M', attrs={'type': 'datetime'})
self.assertHTMLEqual(w.render('date', d), '<input type="datetime" name="date" value="17/09/2007 12:51" />') self.assertHTMLEqual(w.render('date', d), '<input type="datetime" name="date" value="17/09/2007 12:51" />')
self.assertFalse(w._has_changed(d, '17/09/2007 12:51'))
# Make sure a custom format works with _has_changed. The hidden input will use
data = datetime.datetime(2010, 3, 6, 12, 0, 0)
custom_format = '%d.%m.%Y %H:%M'
w = DateTimeInput(format=custom_format)
self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format)))
def test_dateinput(self): def test_dateinput(self):
w = DateInput() w = DateInput()
@ -957,13 +878,6 @@ beatle J R Ringo False""")
# Use 'format' to change the way a value is displayed. # Use 'format' to change the way a value is displayed.
w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'}) w = DateInput(format='%d/%m/%Y', attrs={'type': 'date'})
self.assertHTMLEqual(w.render('date', d), '<input type="date" name="date" value="17/09/2007" />') self.assertHTMLEqual(w.render('date', d), '<input type="date" name="date" value="17/09/2007" />')
self.assertFalse(w._has_changed(d, '17/09/2007'))
# Make sure a custom format works with _has_changed. The hidden input will use
data = datetime.date(2010, 3, 6)
custom_format = '%d.%m.%Y'
w = DateInput(format=custom_format)
self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format)))
def test_timeinput(self): def test_timeinput(self):
w = TimeInput() w = TimeInput()
@ -982,13 +896,6 @@ beatle J R Ringo False""")
# Use 'format' to change the way a value is displayed. # Use 'format' to change the way a value is displayed.
w = TimeInput(format='%H:%M', attrs={'type': 'time'}) w = TimeInput(format='%H:%M', attrs={'type': 'time'})
self.assertHTMLEqual(w.render('time', t), '<input type="time" name="time" value="12:51" />') self.assertHTMLEqual(w.render('time', t), '<input type="time" name="time" value="12:51" />')
self.assertFalse(w._has_changed(t, '12:51'))
# Make sure a custom format works with _has_changed. The hidden input will use
data = datetime.time(13, 0)
custom_format = '%I:%M %p'
w = TimeInput(format=custom_format)
self.assertFalse(w._has_changed(formats.localize_input(data), data.strftime(custom_format)))
def test_splithiddendatetime(self): def test_splithiddendatetime(self):
from django.forms.widgets import SplitHiddenDateTimeWidget from django.forms.widgets import SplitHiddenDateTimeWidget
@ -1016,10 +923,6 @@ class FormsI18NWidgetsTestCase(TestCase):
deactivate() deactivate()
super(FormsI18NWidgetsTestCase, self).tearDown() super(FormsI18NWidgetsTestCase, self).tearDown()
def test_splitdatetime(self):
w = SplitDateTimeWidget(date_format='%d/%m/%Y', time_format='%H:%M')
self.assertTrue(w._has_changed(datetime.datetime(2008, 5, 6, 12, 40, 00), ['06.05.2008', '12:41']))
def test_datetimeinput(self): def test_datetimeinput(self):
w = DateTimeInput() w = DateTimeInput()
d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548) d = datetime.datetime(2007, 9, 17, 12, 51, 34, 482548)