From bc5e4754ebf7416c53459a051511b038998d7c01 Mon Sep 17 00:00:00 2001
From: Simon Charette <charette.s@gmail.com>
Date: Sun, 22 Jul 2012 00:36:32 -0400
Subject: [PATCH] Fixed #5524 -- Invalid forms should keep their valid
`cleaned_data`.
Thanks to @aaugustin for the patch.
---
django/contrib/formtools/tests/__init__.py | 2 +-
django/forms/forms.py | 2 --
django/forms/models.py | 25 ++++++++++++-------------
docs/ref/forms/api.txt | 21 ++++++++++++---------
docs/ref/forms/validation.txt | 10 ++++++----
docs/releases/1.5.txt | 10 ++++++++++
tests/modeltests/model_forms/tests.py | 3 +--
tests/regressiontests/forms/tests/forms.py | 24 ++++--------------------
8 files changed, 46 insertions(+), 51 deletions(-)
diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py
index 3bccb55..ee93479 100644
a
|
b
|
class WizardTests(TestCase):
|
317 | 317 | |
318 | 318 | class WizardWithProcessStep(TestWizardClass): |
319 | 319 | def process_step(self, request, form, step): |
320 | | that.assertTrue(hasattr(form, 'cleaned_data')) |
| 320 | that.assertTrue(form.is_valid()) |
321 | 321 | reached[0] = True |
322 | 322 | |
323 | 323 | wizard = WizardWithProcessStep([WizardPageOneForm, |
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 7942275..9288297 100644
a
|
b
|
class BaseForm(StrAndUnicode):
|
270 | 270 | self._clean_fields() |
271 | 271 | self._clean_form() |
272 | 272 | self._post_clean() |
273 | | if self._errors: |
274 | | del self.cleaned_data |
275 | 273 | |
276 | 274 | def _clean_fields(self): |
277 | 275 | for name, field in self.fields.items(): |
diff --git a/django/forms/models.py b/django/forms/models.py
index 26f8691..7d1809d 100644
a
|
b
|
class BaseModelFormSet(BaseFormSet):
|
504 | 504 | all_unique_checks = set() |
505 | 505 | all_date_checks = set() |
506 | 506 | for form in self.forms: |
507 | | if not hasattr(form, 'cleaned_data'): |
| 507 | if not form.is_valid(): |
508 | 508 | continue |
509 | 509 | exclude = form._get_validation_exclusions() |
510 | 510 | unique_checks, date_checks = form.instance._get_unique_checks(exclude=exclude) |
… |
… |
class BaseModelFormSet(BaseFormSet):
|
516 | 516 | for uclass, unique_check in all_unique_checks: |
517 | 517 | seen_data = set() |
518 | 518 | for form in self.forms: |
519 | | # if the form doesn't have cleaned_data then we ignore it, |
520 | | # it's already invalid |
521 | | if not hasattr(form, "cleaned_data"): |
| 519 | if not form.is_valid(): |
522 | 520 | continue |
523 | 521 | # get data for each field of each of unique_check |
524 | 522 | row_data = tuple([form.cleaned_data[field] for field in unique_check if field in form.cleaned_data]) |
525 | 523 | if row_data and not None in row_data: |
526 | | # if we've aready seen it then we have a uniqueness failure |
| 524 | # if we've already seen it then we have a uniqueness failure |
527 | 525 | if row_data in seen_data: |
528 | 526 | # poke error messages into the right places and mark |
529 | 527 | # the form as invalid |
530 | 528 | errors.append(self.get_unique_error_message(unique_check)) |
531 | 529 | form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()]) |
532 | | del form.cleaned_data |
533 | | break |
| 530 | # remove the data from the cleaned_data dict since it was invalid |
| 531 | for field in unique_check: |
| 532 | if field in form.cleaned_data: |
| 533 | del form.cleaned_data[field] |
534 | 534 | # mark the data as seen |
535 | 535 | seen_data.add(row_data) |
536 | 536 | # iterate over each of the date checks now |
… |
… |
class BaseModelFormSet(BaseFormSet):
|
538 | 538 | seen_data = set() |
539 | 539 | uclass, lookup, field, unique_for = date_check |
540 | 540 | for form in self.forms: |
541 | | # if the form doesn't have cleaned_data then we ignore it, |
542 | | # it's already invalid |
543 | | if not hasattr(self, 'cleaned_data'): |
| 541 | if not form.is_valid(): |
544 | 542 | continue |
545 | 543 | # see if we have data for both fields |
546 | 544 | if (form.cleaned_data and form.cleaned_data[field] is not None |
… |
… |
class BaseModelFormSet(BaseFormSet):
|
554 | 552 | else: |
555 | 553 | date_data = (getattr(form.cleaned_data[unique_for], lookup),) |
556 | 554 | data = (form.cleaned_data[field],) + date_data |
557 | | # if we've aready seen it then we have a uniqueness failure |
| 555 | # if we've already seen it then we have a uniqueness failure |
558 | 556 | if data in seen_data: |
559 | 557 | # poke error messages into the right places and mark |
560 | 558 | # the form as invalid |
561 | 559 | errors.append(self.get_date_error_message(date_check)) |
562 | 560 | form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()]) |
563 | | del form.cleaned_data |
564 | | break |
| 561 | # remove the data from the cleaned_data dict since it was invalid |
| 562 | del form.cleaned_data[field] |
| 563 | # mark the data as seen |
565 | 564 | seen_data.add(data) |
566 | 565 | if errors: |
567 | 566 | raise ValidationError(errors) |
diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt
index 50488b0..777d73e 100644
a
|
b
|
Note that any text-based field -- such as ``CharField`` or ``EmailField`` --
|
199 | 199 | always cleans the input into a Unicode string. We'll cover the encoding |
200 | 200 | implications later in this document. |
201 | 201 | |
202 | | If your data does *not* validate, your ``Form`` instance will not have a |
203 | | ``cleaned_data`` attribute:: |
| 202 | If your data does *not* validate, the ``cleaned_data`` dictionary contains |
| 203 | only the valid fields:: |
204 | 204 | |
205 | 205 | >>> data = {'subject': '', |
206 | 206 | ... 'message': 'Hi there', |
… |
… |
If your data does *not* validate, your ``Form`` instance will not have a
|
210 | 210 | >>> f.is_valid() |
211 | 211 | False |
212 | 212 | >>> f.cleaned_data |
213 | | Traceback (most recent call last): |
214 | | ... |
215 | | AttributeError: 'ContactForm' object has no attribute 'cleaned_data' |
| 213 | {'cc_myself': True, 'message': u'Hi there'} |
| 214 | |
| 215 | .. versionchanged:: 1.5 |
| 216 | |
| 217 | Until Django 1.5, the ``cleaned_data`` attribute wasn't defined at all when |
| 218 | the ``Form`` didn't validate. |
216 | 219 | |
217 | 220 | ``cleaned_data`` will always *only* contain a key for fields defined in the |
218 | 221 | ``Form``, even if you pass extra data when you define the ``Form``. In this |
… |
… |
but ``cleaned_data`` contains only the form's fields::
|
232 | 235 | >>> f.cleaned_data # Doesn't contain extra_field_1, etc. |
233 | 236 | {'cc_myself': True, 'message': u'Hi there', 'sender': u'foo@example.com', 'subject': u'hello'} |
234 | 237 | |
235 | | ``cleaned_data`` will include a key and value for *all* fields defined in the |
236 | | ``Form``, even if the data didn't include a value for fields that are not |
237 | | required. In this example, the data dictionary doesn't include a value for the |
| 238 | When the ``Form`` is valid, ``cleaned_data`` will include a key and value for |
| 239 | *all* its fields, even if the data didn't include a value for some optional |
| 240 | fields. In this example, the data dictionary doesn't include a value for the |
238 | 241 | ``nick_name`` field, but ``cleaned_data`` includes it, with an empty value:: |
239 | 242 | |
240 | 243 | >>> class OptionalPersonForm(Form): |
… |
… |
lazy developers -- they're not the only way a form object can be displayed.
|
583 | 586 | |
584 | 587 | Used to display HTML or access attributes for a single field of a |
585 | 588 | :class:`Form` instance. |
586 | | |
| 589 | |
587 | 590 | The :meth:`__unicode__` and :meth:`__str__` methods of this object displays |
588 | 591 | the HTML for this field. |
589 | 592 | |
diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt
index 97883d7..95424d0 100644
a
|
b
|
Secondly, once we have decided that the combined data in the two fields we are
|
362 | 362 | considering aren't valid, we must remember to remove them from the |
363 | 363 | ``cleaned_data``. |
364 | 364 | |
365 | | In fact, Django will currently completely wipe out the ``cleaned_data`` |
366 | | dictionary if there are any errors in the form. However, this behavior may |
367 | | change in the future, so it's not a bad idea to clean up after yourself in the |
368 | | first place. |
| 365 | .. versionchanged:: 1.5 |
| 366 | |
| 367 | Django used to remove the ``cleaned_data`` attribute entirely if there were |
| 368 | any errors in the form. Since version 1.5, ``cleaned_data`` is present even if |
| 369 | the form doesn't validate, but it contains only field values that did |
| 370 | validate. |
diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt
index fd9ae4f..924d3ba 100644
a
|
b
|
Session not saved on 500 responses
|
188 | 188 | Django's session middleware will skip saving the session data if the |
189 | 189 | response's status code is 500. |
190 | 190 | |
| 191 | `cleaned_data` dictionary kept for invalid forms |
| 192 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 193 | |
| 194 | The :attr:`~django.forms.Form.cleaned_data` dictionary is now always present |
| 195 | after form validation. When the form doesn't validate, it contains only the |
| 196 | fields that passed validation. You should test the success of the validation |
| 197 | with the :meth:`~django.forms.Form.is_valid()` method and not with the |
| 198 | presence or absence of the :attr:`~django.forms.Form.cleaned_data` attribute |
| 199 | on the form. |
| 200 | |
191 | 201 | Miscellaneous |
192 | 202 | ~~~~~~~~~~~~~ |
193 | 203 | |
diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py
index 281316a..6e88ff1 100644
a
|
b
|
class OldFormForXTests(TestCase):
|
637 | 637 | f = BaseCategoryForm({'name': '', 'slug': 'not a slug!', 'url': 'foo'}) |
638 | 638 | self.assertEqual(f.errors['name'], ['This field is required.']) |
639 | 639 | self.assertEqual(f.errors['slug'], ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]) |
640 | | with self.assertRaises(AttributeError): |
641 | | f.cleaned_data |
| 640 | self.assertEqual(f.cleaned_data, {'url': 'foo'}) |
642 | 641 | with self.assertRaises(ValueError): |
643 | 642 | f.save() |
644 | 643 | f = BaseCategoryForm({'name': '', 'slug': '', 'url': 'foo'}) |
diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py
index 7e1c838..a8a28ba 100644
a
|
b
|
class FormsTestCase(TestCase):
|
82 | 82 | self.assertEqual(p.errors['last_name'], ['This field is required.']) |
83 | 83 | self.assertEqual(p.errors['birthday'], ['This field is required.']) |
84 | 84 | self.assertFalse(p.is_valid()) |
85 | | try: |
86 | | p.cleaned_data |
87 | | self.fail('Attempts to access cleaned_data when validation fails should fail.') |
88 | | except AttributeError: |
89 | | pass |
| 85 | self.assertEqual(p.cleaned_data, {}) |
90 | 86 | self.assertHTMLEqual(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> |
91 | 87 | <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> |
92 | 88 | <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>""") |
… |
… |
class FormsTestCase(TestCase):
|
145 | 141 | * This field is required. |
146 | 142 | * birthday |
147 | 143 | * This field is required.""") |
148 | | try: |
149 | | p.cleaned_data |
150 | | self.fail('Attempts to access cleaned_data when validation fails should fail.') |
151 | | except AttributeError: |
152 | | pass |
| 144 | self.assertEqual(p.cleaned_data, {'last_name': 'Lennon'}) |
153 | 145 | self.assertEqual(p['first_name'].errors, ['This field is required.']) |
154 | 146 | self.assertHTMLEqual(p['first_name'].errors.as_ul(), '<ul class="errorlist"><li>This field is required.</li></ul>') |
155 | 147 | self.assertEqual(p['first_name'].errors.as_text(), '* This field is required.') |
… |
… |
class FormsTestCase(TestCase):
|
1678 | 1670 | form = SongForm(data, empty_permitted=False) |
1679 | 1671 | self.assertFalse(form.is_valid()) |
1680 | 1672 | self.assertEqual(form.errors, {'name': ['This field is required.'], 'artist': ['This field is required.']}) |
1681 | | try: |
1682 | | form.cleaned_data |
1683 | | self.fail('Attempts to access cleaned_data when validation fails should fail.') |
1684 | | except AttributeError: |
1685 | | pass |
| 1673 | self.assertEqual(form.cleaned_data, {}) |
1686 | 1674 | |
1687 | 1675 | # Now let's show what happens when empty_permitted=True and the form is empty. |
1688 | 1676 | form = SongForm(data, empty_permitted=True) |
… |
… |
class FormsTestCase(TestCase):
|
1696 | 1684 | form = SongForm(data, empty_permitted=False) |
1697 | 1685 | self.assertFalse(form.is_valid()) |
1698 | 1686 | self.assertEqual(form.errors, {'name': ['This field is required.']}) |
1699 | | try: |
1700 | | form.cleaned_data |
1701 | | self.fail('Attempts to access cleaned_data when validation fails should fail.') |
1702 | | except AttributeError: |
1703 | | pass |
| 1687 | self.assertEqual(form.cleaned_data, {'artist': 'The Doors'}) |
1704 | 1688 | |
1705 | 1689 | # If a field is not given in the data then None is returned for its data. Lets |
1706 | 1690 | # make sure that when checking for empty_permitted that None is treated |