Fixed #16612 -- Improved has_changed detection for localized field values

Thanks Simon Charette for the review.
This commit is contained in:
Claude Paroz 2013-01-26 17:05:47 +01:00
parent 0c82b1dfc4
commit 892bc91cb0
5 changed files with 53 additions and 28 deletions

View File

@ -184,17 +184,13 @@ class Field(object):
# For purposes of seeing whether something has changed, None is # For purposes of seeing whether something has changed, None is
# the same as an empty string, if the data or inital value we get # the same as an empty string, if the data or inital value we get
# is None, replace it w/ ''. # is None, replace it w/ ''.
if data is None: initial_value = initial if initial is not None else ''
data_value = '' try:
else: data = self.to_python(data)
data_value = data except ValidationError:
if initial is None:
initial_value = ''
else:
initial_value = initial
if force_text(initial_value) != force_text(data_value):
return True return True
return False data_value = data if data is not None else ''
return initial_value != data_value
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
result = copy.copy(self) result = copy.copy(self)
@ -392,12 +388,6 @@ 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

View File

@ -345,8 +345,8 @@ class BaseForm(object):
else: else:
initial_prefixed_name = self.add_initial_prefix(name) initial_prefixed_name = self.add_initial_prefix(name)
hidden_widget = field.hidden_widget() hidden_widget = field.hidden_widget()
initial_value = hidden_widget.value_from_datadict( initial_value = field.to_python(hidden_widget.value_from_datadict(
self.data, self.files, initial_prefixed_name) self.data, self.files, initial_prefixed_name))
if hasattr(field.widget, '_has_changed'): if hasattr(field.widget, '_has_changed'):
warnings.warn("The _has_changed method on widgets is deprecated," warnings.warn("The _has_changed method on widgets is deprecated,"
" define it at field level instead.", " define it at field level instead.",

View File

@ -1012,6 +1012,11 @@ class ModelChoiceField(ChoiceField):
def validate(self, value): def validate(self, value):
return Field.validate(self, value) return Field.validate(self, value)
def _has_changed(self, initial, data):
initial_value = initial if initial is not None else ''
data_value = data if data is not None else ''
return force_text(self.prepare_value(initial_value)) != force_text(data_value)
class ModelMultipleChoiceField(ModelChoiceField): class ModelMultipleChoiceField(ModelChoiceField):
"""A MultipleChoiceField whose choices are a model QuerySet.""" """A MultipleChoiceField whose choices are a model QuerySet."""
widget = SelectMultiple widget = SelectMultiple
@ -1059,3 +1064,14 @@ class ModelMultipleChoiceField(ModelChoiceField):
not hasattr(value, '_meta')): not hasattr(value, '_meta')):
return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in value] return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in value]
return super(ModelMultipleChoiceField, self).prepare_value(value) return super(ModelMultipleChoiceField, self).prepare_value(value)
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

View File

@ -35,7 +35,9 @@ 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 import translation
from django.utils._os import upath from django.utils._os import upath
@ -256,6 +258,17 @@ class FieldsTests(SimpleTestCase):
f = FloatField(localize=True) f = FloatField(localize=True)
self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" />') self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" />')
def test_floatfield_changed(self):
f = FloatField()
n = 4.35
self.assertFalse(f._has_changed(n, '4.3500'))
with translation.override('fr'):
with self.settings(USE_L10N=True):
f = FloatField(localize=True)
localized_n = formats.localize_input(n) # -> '4,35' in French
self.assertFalse(f._has_changed(n, localized_n))
# DecimalField ################################################################ # DecimalField ################################################################
def test_decimalfield_1(self): def test_decimalfield_1(self):
@ -346,6 +359,18 @@ class FieldsTests(SimpleTestCase):
f = DecimalField(localize=True) f = DecimalField(localize=True)
self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" />') self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" />')
def test_decimalfield_changed(self):
f = DecimalField(max_digits=2, decimal_places=2)
d = Decimal("0.1")
self.assertFalse(f._has_changed(d, '0.10'))
self.assertTrue(f._has_changed(d, '0.101'))
with translation.override('fr'):
with self.settings(USE_L10N=True):
f = DecimalField(max_digits=2, decimal_places=2, localize=True)
localized_d = formats.localize_input(d) # -> '0,1' in French
self.assertFalse(f._has_changed(d, localized_d))
# DateField ################################################################### # DateField ###################################################################
def test_datefield_1(self): def test_datefield_1(self):
@ -404,7 +429,6 @@ class FieldsTests(SimpleTestCase):
f = DateField(input_formats=[format]) f = DateField(input_formats=[format])
d = datetime.date(2007, 9, 17) d = datetime.date(2007, 9, 17)
self.assertFalse(f._has_changed(d, '17/09/2007')) self.assertFalse(f._has_changed(d, '17/09/2007'))
self.assertFalse(f._has_changed(d.strftime(format), '17/09/2007'))
def test_datefield_strptime(self): def test_datefield_strptime(self):
"""Test that field.strptime doesn't raise an UnicodeEncodeError (#16123)""" """Test that field.strptime doesn't raise an UnicodeEncodeError (#16123)"""
@ -445,14 +469,10 @@ class FieldsTests(SimpleTestCase):
def test_timefield_changed(self): def test_timefield_changed(self):
t1 = datetime.time(12, 51, 34, 482548) t1 = datetime.time(12, 51, 34, 482548)
t2 = datetime.time(12, 51) t2 = datetime.time(12, 51)
format = '%H:%M' f = TimeField(input_formats=['%H:%M', '%H:%M %p'])
f = TimeField(input_formats=[format])
self.assertTrue(f._has_changed(t1, '12:51')) self.assertTrue(f._has_changed(t1, '12:51'))
self.assertFalse(f._has_changed(t2, '12:51')) self.assertFalse(f._has_changed(t2, '12:51'))
self.assertFalse(f._has_changed(t2, '12:51 PM'))
format = '%I:%M %p'
f = TimeField(input_formats=[format])
self.assertFalse(f._has_changed(t2.strftime(format), '12:51 PM'))
# DateTimeField ############################################################### # DateTimeField ###############################################################
@ -518,8 +538,6 @@ class FieldsTests(SimpleTestCase):
f = DateTimeField(input_formats=[format]) f = DateTimeField(input_formats=[format])
d = datetime.datetime(2006, 9, 17, 14, 30, 0) d = datetime.datetime(2006, 9, 17, 14, 30, 0)
self.assertFalse(f._has_changed(d, '2006 09 17 2:30 PM')) 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 ##################################################################

View File

@ -15,7 +15,7 @@ from django.test.utils import override_settings
from django.utils import translation from django.utils import translation
from django.utils.formats import (get_format, date_format, time_format, from django.utils.formats import (get_format, date_format, time_format,
localize, localize_input, iter_format_modules, get_format_modules, localize, localize_input, iter_format_modules, get_format_modules,
number_format, sanitize_separators) number_format, reset_format_cache, sanitize_separators)
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.utils.numberformat import format as nformat from django.utils.numberformat import format as nformat
from django.utils._os import upath from django.utils._os import upath
@ -463,6 +463,7 @@ class FormattingTests(TestCase):
fr_formats.THOUSAND_SEPARATOR = '' fr_formats.THOUSAND_SEPARATOR = ''
fr_formats.FIRST_DAY_OF_WEEK = 0 fr_formats.FIRST_DAY_OF_WEEK = 0
reset_format_cache()
with translation.override('fr'): with translation.override('fr'):
with self.settings(USE_THOUSAND_SEPARATOR=True, THOUSAND_SEPARATOR='!'): with self.settings(USE_THOUSAND_SEPARATOR=True, THOUSAND_SEPARATOR='!'):
self.assertEqual('', get_format('THOUSAND_SEPARATOR')) self.assertEqual('', get_format('THOUSAND_SEPARATOR'))