diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index 42d25fa..0574daa 100644
a
|
b
|
class BaseFormSet(object):
|
175 | 175 | @property |
176 | 176 | def deleted_forms(self): |
177 | 177 | """ |
178 | | Returns a list of forms that have been marked for deletion. Raises an |
179 | | AttributeError if deletion is not allowed. |
| 178 | Returns a list of forms that have been marked for deletion. |
180 | 179 | """ |
181 | 180 | if not self.is_valid() or not self.can_delete: |
182 | | raise AttributeError("'%s' object has no attribute 'deleted_forms'" % self.__class__.__name__) |
| 181 | return [] |
183 | 182 | # construct _deleted_form_indexes which is just a list of form indexes |
184 | 183 | # that have had their deletion widget set to True |
185 | 184 | if not hasattr(self, '_deleted_form_indexes'): |
diff --git a/django/forms/models.py b/django/forms/models.py
index 1aa49ea..e0822ed 100644
a
|
b
|
class BaseModelFormSet(BaseFormSet):
|
500 | 500 | # Collect unique_checks and date_checks to run from all the forms. |
501 | 501 | all_unique_checks = set() |
502 | 502 | all_date_checks = set() |
503 | | for form in self.forms: |
504 | | if not form.is_valid(): |
505 | | continue |
| 503 | forms_to_delete = self.deleted_forms |
| 504 | valid_forms = [form for form in self.forms if form.is_valid() and form not in forms_to_delete] |
| 505 | for form in valid_forms: |
506 | 506 | exclude = form._get_validation_exclusions() |
507 | 507 | unique_checks, date_checks = form.instance._get_unique_checks(exclude=exclude) |
508 | 508 | all_unique_checks = all_unique_checks.union(set(unique_checks)) |
… |
… |
class BaseModelFormSet(BaseFormSet):
|
512 | 512 | # Do each of the unique checks (unique and unique_together) |
513 | 513 | for uclass, unique_check in all_unique_checks: |
514 | 514 | seen_data = set() |
515 | | for form in self.forms: |
516 | | if not form.is_valid(): |
517 | | continue |
| 515 | for form in valid_forms: |
518 | 516 | # get data for each field of each of unique_check |
519 | 517 | row_data = tuple([form.cleaned_data[field] for field in unique_check if field in form.cleaned_data]) |
520 | 518 | if row_data and not None in row_data: |
… |
… |
class BaseModelFormSet(BaseFormSet):
|
534 | 532 | for date_check in all_date_checks: |
535 | 533 | seen_data = set() |
536 | 534 | uclass, lookup, field, unique_for = date_check |
537 | | for form in self.forms: |
538 | | if not form.is_valid(): |
539 | | continue |
| 535 | for form in valid_forms: |
540 | 536 | # see if we have data for both fields |
541 | 537 | if (form.cleaned_data and form.cleaned_data[field] is not None |
542 | 538 | and form.cleaned_data[unique_for] is not None): |
… |
… |
class BaseModelFormSet(BaseFormSet):
|
591 | 587 | return [] |
592 | 588 | |
593 | 589 | saved_instances = [] |
594 | | try: |
595 | | forms_to_delete = self.deleted_forms |
596 | | except AttributeError: |
597 | | forms_to_delete = [] |
| 590 | forms_to_delete = self.deleted_forms |
598 | 591 | for form in self.initial_forms: |
599 | 592 | pk_name = self._pk_field.name |
600 | 593 | raw_pk_value = form._raw_value(pk_name) |
diff --git a/tests/modeltests/model_formsets/tests.py b/tests/modeltests/model_formsets/tests.py
index e28560b..f3a8618 100644
a
|
b
|
class DeletionTests(TestCase):
|
42 | 42 | doesn't cause validation errors. |
43 | 43 | """ |
44 | 44 | PoetFormSet = modelformset_factory(Poet, can_delete=True) |
| 45 | poet = Poet.objects.create(name='test') |
| 46 | # One existing untouched and two new unvalid forms |
45 | 47 | data = { |
46 | | 'form-TOTAL_FORMS': '1', |
47 | | 'form-INITIAL_FORMS': '0', |
| 48 | 'form-TOTAL_FORMS': '3', |
| 49 | 'form-INITIAL_FORMS': '1', |
48 | 50 | 'form-MAX_NUM_FORMS': '0', |
49 | | 'form-0-id': '', |
50 | | 'form-0-name': 'x' * 1000, |
| 51 | 'form-0-id': six.text_type(poet.id), |
| 52 | 'form-0-name': 'test', |
| 53 | 'form-1-id': '', |
| 54 | 'form-1-name': 'x' * 1000, # Too long |
| 55 | 'form-1-id': six.text_type(poet.id), # Violate unique constraint |
| 56 | 'form-1-name': 'test2', |
51 | 57 | } |
52 | 58 | formset = PoetFormSet(data, queryset=Poet.objects.all()) |
53 | 59 | # Make sure this form doesn't pass validation. |
54 | 60 | self.assertEqual(formset.is_valid(), False) |
55 | | self.assertEqual(Poet.objects.count(), 0) |
| 61 | self.assertEqual(Poet.objects.count(), 1) |
56 | 62 | |
57 | 63 | # Then make sure that it *does* pass validation and delete the object, |
58 | | # even though the data isn't actually valid. |
| 64 | # even though the data in new forms aren't actually valid. |
59 | 65 | data['form-0-DELETE'] = 'on' |
| 66 | data['form-1-DELETE'] = 'on' |
| 67 | data['form-2-DELETE'] = 'on' |
60 | 68 | formset = PoetFormSet(data, queryset=Poet.objects.all()) |
61 | 69 | self.assertEqual(formset.is_valid(), True) |
62 | 70 | formset.save() |
… |
… |
class DeletionTests(TestCase):
|
64 | 72 | |
65 | 73 | def test_change_form_deletion_when_invalid(self): |
66 | 74 | """ |
67 | | Make sure that an add form that is filled out, but marked for deletion |
| 75 | Make sure that a change form that is filled out, but marked for deletion |
68 | 76 | doesn't cause validation errors. |
69 | 77 | """ |
70 | 78 | PoetFormSet = modelformset_factory(Poet, can_delete=True) |