From a3b5df8ed503ea559d2ffaca7ec0c735d98f1a38 Mon Sep 17 00:00:00 2001
From: Srinivas Reddy Thatiparthy <thatiparthysreenivas@gmail.com>
Date: Thu, 13 Jul 2017 20:25:32 +0530
Subject: [PATCH] [1.11.x] Fixed #28387 -- Fixed has_changed() for disabled
 form fields that subclass it.

Backport of 5debbdfcc84266703191e084914998e38f5f52eb from master
---
 django/forms/fields.py                                    | 8 ++++++++
 django/forms/models.py                                    | 4 ++++
 docs/releases/1.11.4.txt                                  | 4 ++++
 tests/forms_tests/field_tests/test_booleanfield.py        | 4 ++++
 tests/forms_tests/field_tests/test_filefield.py           | 4 ++++
 tests/forms_tests/field_tests/test_multiplechoicefield.py | 4 ++++
 tests/forms_tests/field_tests/test_multivaluefield.py     | 4 ++++
 tests/model_forms/tests.py                                | 8 ++++++++
 8 files changed, 40 insertions(+)

diff --git a/django/forms/fields.py b/django/forms/fields.py
index 33ed2882a3..f2b5b77185 100644
--- a/django/forms/fields.py
+++ b/django/forms/fields.py
@@ -604,6 +604,8 @@ class FileField(Field):
         return data
 
     def has_changed(self, initial, data):
+        if self.disabled:
+            return False
         if data is None:
             return False
         return True
@@ -724,6 +726,8 @@ class BooleanField(Field):
             raise ValidationError(self.error_messages['required'], code='required')
 
     def has_changed(self, initial, data):
+        if self.disabled:
+            return False
         # Sometimes data or initial may be a string equivalent of a boolean
         # so we should run it through to_python first to get a boolean value
         return self.to_python(initial) != self.to_python(data)
@@ -891,6 +895,8 @@ class MultipleChoiceField(ChoiceField):
                 )
 
     def has_changed(self, initial, data):
+        if self.disabled:
+            return False
         if initial is None:
             initial = []
         if data is None:
@@ -1069,6 +1075,8 @@ class MultiValueField(Field):
         raise NotImplementedError('Subclasses must implement this method.')
 
     def has_changed(self, initial, data):
+        if self.disabled:
+            return False
         if initial is None:
             initial = ['' for x in range(0, len(data))]
         else:
diff --git a/django/forms/models.py b/django/forms/models.py
index 132e4255bb..db710a2ed2 100644
--- a/django/forms/models.py
+++ b/django/forms/models.py
@@ -1244,6 +1244,8 @@ class ModelChoiceField(ChoiceField):
         return Field.validate(self, value)
 
     def has_changed(self, initial, data):
+        if self.disabled:
+            return False
         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)
@@ -1331,6 +1333,8 @@ class ModelMultipleChoiceField(ModelChoiceField):
         return super(ModelMultipleChoiceField, self).prepare_value(value)
 
     def has_changed(self, initial, data):
+        if self.disabled:
+            return False
         if initial is None:
             initial = []
         if data is None:
diff --git a/docs/releases/1.11.4.txt b/docs/releases/1.11.4.txt
index 9448611ecc..c64f5c9c6f 100644
--- a/docs/releases/1.11.4.txt
+++ b/docs/releases/1.11.4.txt
@@ -21,3 +21,7 @@ Bugfixes
 
 * Fixed crash in ``runserver``'s ``autoreload`` with Python 2 on Windows with
   non-``str`` environment variables (:ticket:`28174`).
+
+* Corrected ``Field.has_changed()`` to return ``False`` for disabled form
+  fields: ``BooleanField``, ``MultipleChoiceField``, ``MultiValueField``,
+  ``FileField``, ``ModelChoiceField``, and ``ModelMultipleChoiceField``.
diff --git a/tests/forms_tests/field_tests/test_booleanfield.py b/tests/forms_tests/field_tests/test_booleanfield.py
index e267777b94..d15967c85c 100644
--- a/tests/forms_tests/field_tests/test_booleanfield.py
+++ b/tests/forms_tests/field_tests/test_booleanfield.py
@@ -59,3 +59,7 @@ class BooleanFieldTest(SimpleTestCase):
         self.assertFalse(f.has_changed(True, 'True'))
         self.assertTrue(f.has_changed(False, 'True'))
         self.assertTrue(f.has_changed(True, 'False'))
+
+    def test_disabled_has_changed(self):
+        f = BooleanField(disabled=True)
+        self.assertIs(f.has_changed('True', 'False'), False)
diff --git a/tests/forms_tests/field_tests/test_filefield.py b/tests/forms_tests/field_tests/test_filefield.py
index 2c08075f3f..59c91005d6 100644
--- a/tests/forms_tests/field_tests/test_filefield.py
+++ b/tests/forms_tests/field_tests/test_filefield.py
@@ -77,5 +77,9 @@ class FileFieldTest(SimpleTestCase):
         # with here)
         self.assertTrue(f.has_changed('resume.txt', {'filename': 'resume.txt', 'content': 'My resume'}))
 
+    def test_disabled_has_changed(self):
+        f = FileField(disabled=True)
+        self.assertIs(f.has_changed('x', 'y'), False)
+
     def test_file_picklable(self):
         self.assertIsInstance(pickle.loads(pickle.dumps(FileField())), FileField)
diff --git a/tests/forms_tests/field_tests/test_multiplechoicefield.py b/tests/forms_tests/field_tests/test_multiplechoicefield.py
index 85b7049854..21866b89d1 100644
--- a/tests/forms_tests/field_tests/test_multiplechoicefield.py
+++ b/tests/forms_tests/field_tests/test_multiplechoicefield.py
@@ -70,3 +70,7 @@ class MultipleChoiceFieldTest(SimpleTestCase):
         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']))
+
+    def test_disabled_has_changed(self):
+        f = MultipleChoiceField(choices=[('1', 'One'), ('2', 'Two')], disabled=True)
+        self.assertIs(f.has_changed('x', 'y'), False)
diff --git a/tests/forms_tests/field_tests/test_multivaluefield.py b/tests/forms_tests/field_tests/test_multivaluefield.py
index 3d17168618..d5669a358a 100644
--- a/tests/forms_tests/field_tests/test_multivaluefield.py
+++ b/tests/forms_tests/field_tests/test_multivaluefield.py
@@ -103,6 +103,10 @@ class MultiValueFieldTest(SimpleTestCase):
             ['some text', ['J', 'P'], ['2009-04-25', '11:44:00']],
         ))
 
+    def test_disabled_has_changed(self):
+        f = MultiValueField(fields=(CharField(), CharField()), disabled=True)
+        self.assertIs(f.has_changed(['x', 'x'], ['y', 'y']), False)
+
     def test_form_as_table(self):
         form = ComplexFieldForm()
         self.assertHTMLEqual(
diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py
index f33278f88a..f3317f59de 100644
--- a/tests/model_forms/tests.py
+++ b/tests/model_forms/tests.py
@@ -1708,6 +1708,10 @@ class ModelChoiceFieldTests(TestCase):
             ['Select a valid choice. That choice is not one of the available choices.']
         )
 
+    def test_disabled_modelchoicefield_has_changed(self):
+        field = forms.ModelChoiceField(Author.objects.all(), disabled=True)
+        self.assertIs(field.has_changed('x', 'y'), False)
+
     def test_disabled_multiplemodelchoicefield(self):
         class ArticleForm(forms.ModelForm):
             categories = forms.ModelMultipleChoiceField(Category.objects.all(), required=False)
@@ -1733,6 +1737,10 @@ class ModelChoiceFieldTests(TestCase):
         self.assertEqual(form.errors, {})
         self.assertEqual([x.pk for x in form.cleaned_data['categories']], [category1.pk])
 
+    def test_disabled_modelmultiplechoicefield_has_changed(self):
+        field = forms.ModelMultipleChoiceField(Author.objects.all(), disabled=True)
+        self.assertIs(field.has_changed('x', 'y'), False)
+
     def test_modelchoicefield_iterator(self):
         """
         Iterator defaults to ModelChoiceIterator and can be overridden with