From 892bc91cb0036d6868081363628f65094c4790d6 Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Sat, 26 Jan 2013 17:05:47 +0100 Subject: [PATCH] Fixed #16612 -- Improved has_changed detection for localized field values Thanks Simon Charette for the review. --- django/forms/fields.py | 22 ++++++------------- django/forms/forms.py | 4 ++-- django/forms/models.py | 16 ++++++++++++++ tests/forms_tests/tests/fields.py | 36 +++++++++++++++++++++++-------- tests/i18n/tests.py | 3 ++- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/django/forms/fields.py b/django/forms/fields.py index 277f0b093e..c7ed085b16 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -184,17 +184,13 @@ class Field(object): # 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): + initial_value = initial if initial is not None else '' + try: + data = self.to_python(data) + except ValidationError: return True - return False + data_value = data if data is not None else '' + return initial_value != data_value def __deepcopy__(self, memo): result = copy.copy(self) @@ -392,12 +388,6 @@ class BaseTemporalField(Field): def strptime(self, value, format): 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): widget = DateInput diff --git a/django/forms/forms.py b/django/forms/forms.py index 20a2e426bb..38601432bc 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -345,8 +345,8 @@ class BaseForm(object): else: initial_prefixed_name = self.add_initial_prefix(name) hidden_widget = field.hidden_widget() - initial_value = hidden_widget.value_from_datadict( - self.data, self.files, initial_prefixed_name) + initial_value = field.to_python(hidden_widget.value_from_datadict( + self.data, self.files, initial_prefixed_name)) if hasattr(field.widget, '_has_changed'): warnings.warn("The _has_changed method on widgets is deprecated," " define it at field level instead.", diff --git a/django/forms/models.py b/django/forms/models.py index 3905e9e902..1cea110df4 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -1012,6 +1012,11 @@ class ModelChoiceField(ChoiceField): def 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): """A MultipleChoiceField whose choices are a model QuerySet.""" widget = SelectMultiple @@ -1059,3 +1064,14 @@ class ModelMultipleChoiceField(ModelChoiceField): not hasattr(value, '_meta')): return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in 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 diff --git a/tests/forms_tests/tests/fields.py b/tests/forms_tests/tests/fields.py index b0e299983b..3d3206ef85 100644 --- a/tests/forms_tests/tests/fields.py +++ b/tests/forms_tests/tests/fields.py @@ -35,7 +35,9 @@ from decimal import Decimal from django.core.files.uploadedfile import SimpleUploadedFile from django.forms import * from django.test import SimpleTestCase +from django.utils import formats from django.utils import six +from django.utils import translation from django.utils._os import upath @@ -256,6 +258,17 @@ class FieldsTests(SimpleTestCase): f = FloatField(localize=True) self.assertWidgetRendersTo(f, '') + 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 ################################################################ def test_decimalfield_1(self): @@ -346,6 +359,18 @@ class FieldsTests(SimpleTestCase): f = DecimalField(localize=True) self.assertWidgetRendersTo(f, '') + 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 ################################################################### def test_datefield_1(self): @@ -404,7 +429,6 @@ class FieldsTests(SimpleTestCase): 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')) def test_datefield_strptime(self): """Test that field.strptime doesn't raise an UnicodeEncodeError (#16123)""" @@ -445,14 +469,10 @@ class FieldsTests(SimpleTestCase): 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]) + f = TimeField(input_formats=['%H:%M', '%H:%M %p']) 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')) + self.assertFalse(f._has_changed(t2, '12:51 PM')) # DateTimeField ############################################################### @@ -518,8 +538,6 @@ class FieldsTests(SimpleTestCase): 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 ################################################################## diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 3038c4813c..d26a201efa 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -15,7 +15,7 @@ from django.test.utils import override_settings from django.utils import translation from django.utils.formats import (get_format, date_format, time_format, 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.numberformat import format as nformat from django.utils._os import upath @@ -463,6 +463,7 @@ class FormattingTests(TestCase): fr_formats.THOUSAND_SEPARATOR = '' fr_formats.FIRST_DAY_OF_WEEK = 0 + reset_format_cache() with translation.override('fr'): with self.settings(USE_THOUSAND_SEPARATOR=True, THOUSAND_SEPARATOR='!'): self.assertEqual('', get_format('THOUSAND_SEPARATOR'))