Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #16612 -- Improved has_changed detection for localized field va…

…lues

Thanks Simon Charette for the review.
  • Loading branch information...
commit 892bc91cb0036d6868081363628f65094c4790d6 1 parent 0c82b1d
Claude Paroz authored
22  django/forms/fields.py
@@ -184,17 +184,13 @@ def _has_changed(self, initial, data):
184 184
         # For purposes of seeing whether something has changed, None is
185 185
         # the same as an empty string, if the data or inital value we get
186 186
         # is None, replace it w/ ''.
187  
-        if data is None:
188  
-            data_value = ''
189  
-        else:
190  
-            data_value = data
191  
-        if initial is None:
192  
-            initial_value = ''
193  
-        else:
194  
-            initial_value = initial
195  
-        if force_text(initial_value) != force_text(data_value):
  187
+        initial_value = initial if initial is not None else ''
  188
+        try:
  189
+            data = self.to_python(data)
  190
+        except ValidationError:
196 191
             return True
197  
-        return False
  192
+        data_value = data if data is not None else ''
  193
+        return initial_value != data_value
198 194
 
199 195
     def __deepcopy__(self, memo):
200 196
         result = copy.copy(self)
@@ -392,12 +388,6 @@ def to_python(self, value):
392 388
     def strptime(self, value, format):
393 389
         raise NotImplementedError('Subclasses must define this method.')
394 390
 
395  
-    def _has_changed(self, initial, data):
396  
-        try:
397  
-            data = self.to_python(data)
398  
-        except ValidationError:
399  
-            return True
400  
-        return self.to_python(initial) != data
401 391
 
402 392
 class DateField(BaseTemporalField):
403 393
     widget = DateInput
4  django/forms/forms.py
@@ -345,8 +345,8 @@ def changed_data(self):
345 345
                 else:
346 346
                     initial_prefixed_name = self.add_initial_prefix(name)
347 347
                     hidden_widget = field.hidden_widget()
348  
-                    initial_value = hidden_widget.value_from_datadict(
349  
-                        self.data, self.files, initial_prefixed_name)
  348
+                    initial_value = field.to_python(hidden_widget.value_from_datadict(
  349
+                        self.data, self.files, initial_prefixed_name))
350 350
                 if hasattr(field.widget, '_has_changed'):
351 351
                     warnings.warn("The _has_changed method on widgets is deprecated,"
352 352
                         " define it at field level instead.",
16  django/forms/models.py
@@ -1012,6 +1012,11 @@ def to_python(self, value):
1012 1012
     def validate(self, value):
1013 1013
         return Field.validate(self, value)
1014 1014
 
  1015
+    def _has_changed(self, initial, data):
  1016
+        initial_value = initial if initial is not None else ''
  1017
+        data_value = data if data is not None else ''
  1018
+        return force_text(self.prepare_value(initial_value)) != force_text(data_value)
  1019
+
1015 1020
 class ModelMultipleChoiceField(ModelChoiceField):
1016 1021
     """A MultipleChoiceField whose choices are a model QuerySet."""
1017 1022
     widget = SelectMultiple
@@ -1059,3 +1064,14 @@ def prepare_value(self, value):
1059 1064
                 not hasattr(value, '_meta')):
1060 1065
             return [super(ModelMultipleChoiceField, self).prepare_value(v) for v in value]
1061 1066
         return super(ModelMultipleChoiceField, self).prepare_value(value)
  1067
+
  1068
+    def _has_changed(self, initial, data):
  1069
+        if initial is None:
  1070
+            initial = []
  1071
+        if data is None:
  1072
+            data = []
  1073
+        if len(initial) != len(data):
  1074
+            return True
  1075
+        initial_set = set([force_text(value) for value in initial])
  1076
+        data_set = set([force_text(value) for value in data])
  1077
+        return data_set != initial_set
36  tests/forms_tests/tests/fields.py
@@ -35,7 +35,9 @@
35 35
 from django.core.files.uploadedfile import SimpleUploadedFile
36 36
 from django.forms import *
37 37
 from django.test import SimpleTestCase
  38
+from django.utils import formats
38 39
 from django.utils import six
  40
+from django.utils import translation
39 41
 from django.utils._os import upath
40 42
 
41 43
 
@@ -256,6 +258,17 @@ def test_floatfield_localized(self):
256 258
         f = FloatField(localize=True)
257 259
         self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" />')
258 260
 
  261
+    def test_floatfield_changed(self):
  262
+        f = FloatField()
  263
+        n = 4.35
  264
+        self.assertFalse(f._has_changed(n, '4.3500'))
  265
+
  266
+        with translation.override('fr'):
  267
+            with self.settings(USE_L10N=True):
  268
+                f = FloatField(localize=True)
  269
+                localized_n = formats.localize_input(n)  # -> '4,35' in French
  270
+                self.assertFalse(f._has_changed(n, localized_n))
  271
+
259 272
     # DecimalField ################################################################
260 273
 
261 274
     def test_decimalfield_1(self):
@@ -346,6 +359,18 @@ def test_decimalfield_localized(self):
346 359
         f = DecimalField(localize=True)
347 360
         self.assertWidgetRendersTo(f, '<input id="id_f" name="f" type="text" />')
348 361
 
  362
+    def test_decimalfield_changed(self):
  363
+        f = DecimalField(max_digits=2, decimal_places=2)
  364
+        d = Decimal("0.1")
  365
+        self.assertFalse(f._has_changed(d, '0.10'))
  366
+        self.assertTrue(f._has_changed(d, '0.101'))
  367
+
  368
+        with translation.override('fr'):
  369
+            with self.settings(USE_L10N=True):
  370
+                f = DecimalField(max_digits=2, decimal_places=2, localize=True)
  371
+                localized_d = formats.localize_input(d)  # -> '0,1' in French
  372
+                self.assertFalse(f._has_changed(d, localized_d))
  373
+
349 374
     # DateField ###################################################################
350 375
 
351 376
     def test_datefield_1(self):
@@ -404,7 +429,6 @@ def test_datefield_changed(self):
404 429
         f = DateField(input_formats=[format])
405 430
         d = datetime.date(2007, 9, 17)
406 431
         self.assertFalse(f._has_changed(d, '17/09/2007'))
407  
-        self.assertFalse(f._has_changed(d.strftime(format), '17/09/2007'))
408 432
 
409 433
     def test_datefield_strptime(self):
410 434
         """Test that field.strptime doesn't raise an UnicodeEncodeError (#16123)"""
@@ -445,14 +469,10 @@ def test_timefield_3(self):
445 469
     def test_timefield_changed(self):
446 470
         t1 = datetime.time(12, 51, 34, 482548)
447 471
         t2 = datetime.time(12, 51)
448  
-        format = '%H:%M'
449  
-        f = TimeField(input_formats=[format])
  472
+        f = TimeField(input_formats=['%H:%M', '%H:%M %p'])
450 473
         self.assertTrue(f._has_changed(t1, '12:51'))
451 474
         self.assertFalse(f._has_changed(t2, '12:51'))
452  
-
453  
-        format = '%I:%M %p'
454  
-        f = TimeField(input_formats=[format])
455  
-        self.assertFalse(f._has_changed(t2.strftime(format), '12:51 PM'))
  475
+        self.assertFalse(f._has_changed(t2, '12:51 PM'))
456 476
 
457 477
     # DateTimeField ###############################################################
458 478
 
@@ -518,8 +538,6 @@ def test_datetimefield_changed(self):
518 538
         f = DateTimeField(input_formats=[format])
519 539
         d = datetime.datetime(2006, 9, 17, 14, 30, 0)
520 540
         self.assertFalse(f._has_changed(d, '2006 09 17 2:30 PM'))
521  
-        # Initial value may be a string from a hidden input
522  
-        self.assertFalse(f._has_changed(d.strftime(format), '2006 09 17 2:30 PM'))
523 541
 
524 542
     # RegexField ##################################################################
525 543
 
3  tests/i18n/tests.py
@@ -15,7 +15,7 @@
15 15
 from django.utils import translation
16 16
 from django.utils.formats import (get_format, date_format, time_format,
17 17
     localize, localize_input, iter_format_modules, get_format_modules,
18  
-    number_format, sanitize_separators)
  18
+    number_format, reset_format_cache, sanitize_separators)
19 19
 from django.utils.importlib import import_module
20 20
 from django.utils.numberformat import format as nformat
21 21
 from django.utils._os import upath
@@ -463,6 +463,7 @@ def test_false_like_locale_formats(self):
463 463
         fr_formats.THOUSAND_SEPARATOR = ''
464 464
         fr_formats.FIRST_DAY_OF_WEEK = 0
465 465
 
  466
+        reset_format_cache()
466 467
         with translation.override('fr'):
467 468
             with self.settings(USE_THOUSAND_SEPARATOR=True, THOUSAND_SEPARATOR='!'):
468 469
                 self.assertEqual('', get_format('THOUSAND_SEPARATOR'))

0 notes on commit 892bc91

Please sign in to comment.
Something went wrong with that request. Please try again.