Index: docs/releases/1.4.txt
===================================================================
--- docs/releases/1.4.txt	(révision 17269)
+++ docs/releases/1.4.txt	(copie de travail)
@@ -854,6 +854,16 @@
 If you are using PUT or DELETE methods in AJAX applications, please see the
 :ref:`instructions about using AJAX and CSRF <csrf-ajax>`.
 
+`cleaned_data` dictionary kept for invalid forms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The :attr:`~django.forms.Form.cleaned_data` dictionary is now always present
+after form validation. When the form doesn't validate, it contains only the
+fields that passed validation. You should test the success of the validation
+with the :meth:`~django.forms.Form.is_valid()` method and not with the
+presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute
+on the form.
+
 ``django.core.template_loaders``
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
Index: docs/ref/forms/api.txt
===================================================================
--- docs/ref/forms/api.txt	(révision 17269)
+++ docs/ref/forms/api.txt	(copie de travail)
@@ -199,8 +199,8 @@
 always cleans the input into a Unicode string. We'll cover the encoding
 implications later in this document.
 
-If your data does *not* validate, your ``Form`` instance will not have a
-``cleaned_data`` attribute::
+If your data does *not* validate, the ``cleaned_data`` dictionary contains
+only the valid fields::
 
     >>> data = {'subject': '',
     ...         'message': 'Hi there',
@@ -210,10 +210,13 @@
     >>> f.is_valid()
     False
     >>> f.cleaned_data
-    Traceback (most recent call last):
-    ...
-    AttributeError: 'ContactForm' object has no attribute 'cleaned_data'
+    {'cc_myself': True, 'message': u'Hi there'}
 
+.. versionchanged:: 1.4
+
+Until Django 1.4, the ``cleaned_data`` attribute wasn't defined at all when
+the ``Form`` didn't validate.
+
 ``cleaned_data`` will always *only* contain a key for fields defined in the
 ``Form``, even if you pass extra data when you define the ``Form``. In this
 example, we pass a bunch of extra fields to the ``ContactForm`` constructor,
@@ -232,9 +235,9 @@
     >>> f.cleaned_data # Doesn't contain extra_field_1, etc.
     {'cc_myself': True, 'message': u'Hi there', 'sender': u'foo@example.com', 'subject': u'hello'}
 
-``cleaned_data`` will include a key and value for *all* fields defined in the
-``Form``, even if the data didn't include a value for fields that are not
-required. In this example, the data dictionary doesn't include a value for the
+When the ``Form`` is valid, ``cleaned_data`` will include a key and value for
+*all* its fields, even if the data didn't include a value for some optional
+fields. In this example, the data dictionary doesn't include a value for the
 ``nick_name`` field, but ``cleaned_data`` includes it, with an empty value::
 
     >>> class OptionalPersonForm(Form):
@@ -585,7 +588,7 @@
 
    Used to display HTML or access attributes for a single field of a
    :class:`Form` instance.
-   
+
    The :meth:`__unicode__` and :meth:`__str__` methods of this object displays
    the HTML for this field.
 
Index: docs/ref/forms/validation.txt
===================================================================
--- docs/ref/forms/validation.txt	(révision 17269)
+++ docs/ref/forms/validation.txt	(copie de travail)
@@ -360,7 +360,9 @@
 considering aren't valid, we must remember to remove them from the
 ``cleaned_data``.
 
-In fact, Django will currently completely wipe out the ``cleaned_data``
-dictionary if there are any errors in the form. However, this behavior may
-change in the future, so it's not a bad idea to clean up after yourself in the
-first place.
+.. versionchanged:: 1.4
+
+Django used to remove the ``cleaned_data`` attribute entirely if there were
+any errors in the form. Since version 1.4, ``cleaned_data`` is present even if
+the form doesn't validate, but it contains only field values that did
+validate.
Index: django/forms/models.py
===================================================================
--- django/forms/models.py	(révision 17269)
+++ django/forms/models.py	(copie de travail)
@@ -497,7 +497,7 @@
         all_unique_checks = set()
         all_date_checks = set()
         for form in self.forms:
-            if not hasattr(form, 'cleaned_data'):
+            if not form.is_valid():
                 continue
             exclude = form._get_validation_exclusions()
             unique_checks, date_checks = form.instance._get_unique_checks(exclude=exclude)
@@ -509,21 +509,21 @@
         for uclass, unique_check in all_unique_checks:
             seen_data = set()
             for form in self.forms:
-                # if the form doesn't have cleaned_data then we ignore it,
-                # it's already invalid
-                if not hasattr(form, "cleaned_data"):
+                if not form.is_valid():
                     continue
                 # get data for each field of each of unique_check
                 row_data = tuple([form.cleaned_data[field] for field in unique_check if field in form.cleaned_data])
                 if row_data and not None in row_data:
-                    # if we've aready seen it then we have a uniqueness failure
+                    # if we've already seen it then we have a uniqueness failure
                     if row_data in seen_data:
                         # poke error messages into the right places and mark
                         # the form as invalid
                         errors.append(self.get_unique_error_message(unique_check))
                         form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()])
-                        del form.cleaned_data
-                        break
+                        # remove the data from the cleaned_data dict since it was invalid
+                        for field in unique_check:
+                            if field in form.cleaned_data:
+                                del form.cleaned_data[field]
                     # mark the data as seen
                     seen_data.add(row_data)
         # iterate over each of the date checks now
@@ -531,9 +531,7 @@
             seen_data = set()
             uclass, lookup, field, unique_for = date_check
             for form in self.forms:
-                # if the form doesn't have cleaned_data then we ignore it,
-                # it's already invalid
-                if not hasattr(self, 'cleaned_data'):
+                if not form.is_valid():
                     continue
                 # see if we have data for both fields
                 if (form.cleaned_data and form.cleaned_data[field] is not None
@@ -547,14 +545,15 @@
                     else:
                         date_data = (getattr(form.cleaned_data[unique_for], lookup),)
                     data = (form.cleaned_data[field],) + date_data
-                    # if we've aready seen it then we have a uniqueness failure
+                    # if we've already seen it then we have a uniqueness failure
                     if data in seen_data:
                         # poke error messages into the right places and mark
                         # the form as invalid
                         errors.append(self.get_date_error_message(date_check))
                         form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()])
-                        del form.cleaned_data
-                        break
+                        # remove the data from the cleaned_data dict since it was invalid
+                        del form.cleaned_data[field]
+                    # mark the data as seen
                     seen_data.add(data)
         if errors:
             raise ValidationError(errors)
Index: django/forms/forms.py
===================================================================
--- django/forms/forms.py	(révision 17269)
+++ django/forms/forms.py	(copie de travail)
@@ -270,8 +270,6 @@
         self._clean_fields()
         self._clean_form()
         self._post_clean()
-        if self._errors:
-            del self.cleaned_data
 
     def _clean_fields(self):
         for name, field in self.fields.items():
Index: django/contrib/formtools/tests/__init__.py
===================================================================
--- django/contrib/formtools/tests/__init__.py	(révision 17269)
+++ django/contrib/formtools/tests/__init__.py	(copie de travail)
@@ -362,7 +362,7 @@
 
         class WizardWithProcessStep(TestWizardClass):
             def process_step(self, request, form, step):
-                that.assertTrue(hasattr(form, 'cleaned_data'))
+                that.assertTrue(form.is_valid())
                 reached[0] = True
 
         wizard = WizardWithProcessStep([WizardPageOneForm,
Index: tests/modeltests/model_forms/tests.py
===================================================================
--- tests/modeltests/model_forms/tests.py	(révision 17269)
+++ tests/modeltests/model_forms/tests.py	(copie de travail)
@@ -637,8 +637,7 @@
         f = BaseCategoryForm({'name': '', 'slug': 'not a slug!', 'url': 'foo'})
         self.assertEqual(f.errors['name'], [u'This field is required.'])
         self.assertEqual(f.errors['slug'], [u"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."])
-        with self.assertRaises(AttributeError):
-            f.cleaned_data
+        self.assertEqual(f.cleaned_data, {'url': u'foo'})
         with self.assertRaises(ValueError):
             f.save()
         f = BaseCategoryForm({'name': '', 'slug': '', 'url': 'foo'})
Index: tests/regressiontests/forms/tests/forms.py
===================================================================
--- tests/regressiontests/forms/tests/forms.py	(révision 17269)
+++ tests/regressiontests/forms/tests/forms.py	(copie de travail)
@@ -79,11 +79,7 @@
         self.assertEqual(p.errors['last_name'], [u'This field is required.'])
         self.assertEqual(p.errors['birthday'], [u'This field is required.'])
         self.assertFalse(p.is_valid())
-        try:
-            p.cleaned_data
-            self.fail('Attempts to access cleaned_data when validation fails should fail.')
-        except AttributeError:
-            pass
+        self.assertEqual(p.cleaned_data, {})
         self.assertEqual(str(p), """<tr><th><label for="id_first_name">First name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="first_name" id="id_first_name" /></td></tr>
 <tr><th><label for="id_last_name">Last name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="last_name" id="id_last_name" /></td></tr>
 <tr><th><label for="id_birthday">Birthday:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="birthday" id="id_birthday" /></td></tr>""")
@@ -142,11 +138,7 @@
   * This field is required.
 * birthday
   * This field is required.""")
-        try:
-            p.cleaned_data
-            self.fail('Attempts to access cleaned_data when validation fails should fail.')
-        except AttributeError:
-            pass
+        self.assertEqual(p.cleaned_data, {'last_name': u'Lennon'})
         self.assertEqual(p['first_name'].errors, [u'This field is required.'])
         self.assertEqual(p['first_name'].errors.as_ul(), u'<ul class="errorlist"><li>This field is required.</li></ul>')
         self.assertEqual(p['first_name'].errors.as_text(), u'* This field is required.')
@@ -1672,11 +1664,7 @@
         form = SongForm(data, empty_permitted=False)
         self.assertFalse(form.is_valid())
         self.assertEqual(form.errors, {'name': [u'This field is required.'], 'artist': [u'This field is required.']})
-        try:
-            form.cleaned_data
-            self.fail('Attempts to access cleaned_data when validation fails should fail.')
-        except AttributeError:
-            pass
+        self.assertEqual(form.cleaned_data, {})
 
         # Now let's show what happens when empty_permitted=True and the form is empty.
         form = SongForm(data, empty_permitted=True)
@@ -1690,11 +1678,7 @@
         form = SongForm(data, empty_permitted=False)
         self.assertFalse(form.is_valid())
         self.assertEqual(form.errors, {'name': [u'This field is required.']})
-        try:
-            form.cleaned_data
-            self.fail('Attempts to access cleaned_data when validation fails should fail.')
-        except AttributeError:
-            pass
+        self.assertEqual(form.cleaned_data, {'artist': u'The Doors'})
 
         # If a field is not given in the data then None is returned for its data. Lets
         # make sure that when checking for empty_permitted that None is treated
