Skip to content

Commit 2a3e1a5

Browse files
committed
Suppress validation errors of descendent inlines of deleted forms
fixes #101
1 parent b38f84a commit 2a3e1a5

File tree

6 files changed

+131
-0
lines changed

6 files changed

+131
-0
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ Changelog
1010
``autocomplete_lookup_fields`` (`#114`_)
1111
* Fixed: (grappelli) Collapsible tabular inlines with
1212
``NestedTabularInline.classes`` now work. (`#90`_)
13+
* Fixed: Suppress validation errors of inlines nested beneath deleted inlines
14+
(`#101`_)
1315

1416
.. _#90: https://github.com/theatlantic/django-nested-admin/issues/90
17+
.. _#101: https://github.com/theatlantic/django-nested-admin/issues/101
1518
.. _#114: https://github.com/theatlantic/django-nested-admin/issues/114
1619
.. _#118: https://github.com/theatlantic/django-nested-admin/issues/118
1720
.. _#122: https://github.com/theatlantic/django-nested-admin/issues/122

nested_admin/__init__.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,30 @@ def __getattr__(self, name):
9595
all_valid_patch_modules.append(admin_module)
9696

9797

98+
def descend_form(form):
99+
for formset in getattr(form, 'nested_formsets', None) or []:
100+
for child_formset, child_form in descend_formset(formset):
101+
yield (child_formset, child_form)
102+
103+
104+
def descend_formset(formset):
105+
for form in formset:
106+
yield (formset, form)
107+
for child_formset, child_form in descend_form(form):
108+
yield child_formset, child_form
109+
110+
111+
def patch_delete_children_empty_permitted(formsets):
112+
"""Set empty_permitted=True for descendent forms of forms that are to be deleted"""
113+
for top_level_formset in formsets:
114+
for formset, form in descend_formset(top_level_formset):
115+
formset._errors = None
116+
form._errors = None
117+
if formset.can_delete and formset._should_delete_form(form):
118+
for _, child_form in descend_form(form):
119+
child_form.empty_permitted = True
120+
121+
98122
@monkeybiz.patch(all_valid_patch_modules)
99123
def all_valid(original_all_valid, formsets):
100124
"""
@@ -104,8 +128,16 @@ def all_valid(original_all_valid, formsets):
104128
This causes a bug when one of the parent forms has empty_permitted == True,
105129
which happens if it is an "extra" form in the formset and its index
106130
is >= the formset's min_num.
131+
132+
Also hooks into the original validation to suppress validation errors thrown
133+
by descendent inlines of deleted forms.
107134
"""
108135
if not original_all_valid(formsets):
136+
if len(formsets) and getattr(formsets[0], 'data', None):
137+
has_delete = any(k for k in formsets[0].data if k.endswith('-DELETE'))
138+
if has_delete:
139+
patch_delete_children_empty_permitted(formsets)
140+
return original_all_valid(formsets)
109141
return False
110142

111143
for formset in formsets:

nested_admin/tests/nested_delete_validationerrors/__init__.py

Whitespace-only changes.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from django.contrib import admin
2+
import nested_admin
3+
from .models import Parent, Child, GrandChild
4+
5+
6+
class GrandChildInline(nested_admin.NestedStackedInline):
7+
model = GrandChild
8+
extra = 0
9+
min_num = 1
10+
11+
12+
class ChildInline(nested_admin.NestedStackedInline):
13+
model = Child
14+
inlines = [GrandChildInline]
15+
extra = 0
16+
17+
18+
@admin.register(Parent)
19+
class ParentAdmin(nested_admin.NestedModelAdmin):
20+
inlines = [ChildInline]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import unicode_literals
2+
3+
from django.db import models
4+
from django.db.models import ForeignKey, CASCADE
5+
from nested_admin.tests.compat import python_2_unicode_compatible
6+
7+
8+
@python_2_unicode_compatible
9+
class Parent(models.Model):
10+
name = models.CharField(max_length=128)
11+
12+
def __str__(self):
13+
return self.name
14+
15+
16+
@python_2_unicode_compatible
17+
class Child(models.Model):
18+
name = models.CharField(max_length=128)
19+
parent = ForeignKey(Parent, on_delete=CASCADE, related_name='children')
20+
position = models.PositiveIntegerField()
21+
22+
class Meta:
23+
ordering = ['position']
24+
25+
def __str__(self):
26+
return self.name
27+
28+
29+
@python_2_unicode_compatible
30+
class GrandChild(models.Model):
31+
name = models.CharField(max_length=128)
32+
parent = ForeignKey(Child, on_delete=CASCADE, related_name='children')
33+
position = models.PositiveIntegerField()
34+
35+
class Meta:
36+
ordering = ['position']
37+
38+
def __str__(self):
39+
return self.name
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import time
2+
from unittest import SkipTest
3+
4+
from django.conf import settings
5+
6+
from nested_admin.tests.base import BaseNestedAdminTestCase
7+
from .models import Parent, Child, GrandChild
8+
9+
10+
class TestDeleteNestedInlineMinNumRequirement(BaseNestedAdminTestCase):
11+
12+
root_model = Parent
13+
nested_models = (Child, GrandChild)
14+
15+
def test_min_num_delete_bug(self):
16+
"""It should be possible to delete inlines, even if min_num requirement not met"""
17+
rhea = Parent.objects.create(name='Rhea')
18+
poseidon = Child.objects.create(name='Poseidon', parent=rhea, position=0)
19+
zeus = Child.objects.create(name='Zeus', parent=rhea, position=1)
20+
demeter = Child.objects.create(name='Demeter', parent=rhea, position=2)
21+
22+
GrandChild.objects.create(name='Apollo', parent=zeus, position=0)
23+
GrandChild.objects.create(name='Persephone', parent=demeter, position=0)
24+
25+
self.load_admin(rhea)
26+
self.delete_inline([0])
27+
self.save_form()
28+
29+
validation_errors = self.selenium.execute_script(
30+
"return $('ul.errorlist li').length")
31+
32+
self.assertEqual(
33+
0, validation_errors, "Save should have completed without validation errors")
34+
35+
children = Child.objects.filter(parent=rhea)
36+
self.assertNotEqual(3, len(children), "Child with empty grandchild was not deleted")
37+
self.assertEqual(2, len(children))

0 commit comments

Comments
 (0)