Skip to content

Commit

Permalink
Suppress validation errors of descendent inlines of deleted forms
Browse files Browse the repository at this point in the history
fixes #101
  • Loading branch information
fdintino committed Apr 8, 2019
1 parent b38f84a commit 2a3e1a5
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -10,8 +10,11 @@ Changelog
``autocomplete_lookup_fields`` (`#114`_)
* Fixed: (grappelli) Collapsible tabular inlines with
``NestedTabularInline.classes`` now work. (`#90`_)
* Fixed: Suppress validation errors of inlines nested beneath deleted inlines
(`#101`_)

.. _#90: https://github.com/theatlantic/django-nested-admin/issues/90
.. _#101: https://github.com/theatlantic/django-nested-admin/issues/101
.. _#114: https://github.com/theatlantic/django-nested-admin/issues/114
.. _#118: https://github.com/theatlantic/django-nested-admin/issues/118
.. _#122: https://github.com/theatlantic/django-nested-admin/issues/122
Expand Down
32 changes: 32 additions & 0 deletions nested_admin/__init__.py
Expand Up @@ -95,6 +95,30 @@ def __getattr__(self, name):
all_valid_patch_modules.append(admin_module)


def descend_form(form):
for formset in getattr(form, 'nested_formsets', None) or []:
for child_formset, child_form in descend_formset(formset):
yield (child_formset, child_form)


def descend_formset(formset):
for form in formset:
yield (formset, form)
for child_formset, child_form in descend_form(form):
yield child_formset, child_form


def patch_delete_children_empty_permitted(formsets):
"""Set empty_permitted=True for descendent forms of forms that are to be deleted"""
for top_level_formset in formsets:
for formset, form in descend_formset(top_level_formset):
formset._errors = None
form._errors = None
if formset.can_delete and formset._should_delete_form(form):
for _, child_form in descend_form(form):
child_form.empty_permitted = True


@monkeybiz.patch(all_valid_patch_modules)
def all_valid(original_all_valid, formsets):
"""
Expand All @@ -104,8 +128,16 @@ def all_valid(original_all_valid, formsets):
This causes a bug when one of the parent forms has empty_permitted == True,
which happens if it is an "extra" form in the formset and its index
is >= the formset's min_num.
Also hooks into the original validation to suppress validation errors thrown
by descendent inlines of deleted forms.
"""
if not original_all_valid(formsets):
if len(formsets) and getattr(formsets[0], 'data', None):
has_delete = any(k for k in formsets[0].data if k.endswith('-DELETE'))
if has_delete:
patch_delete_children_empty_permitted(formsets)
return original_all_valid(formsets)
return False

for formset in formsets:
Expand Down
Empty file.
20 changes: 20 additions & 0 deletions nested_admin/tests/nested_delete_validationerrors/admin.py
@@ -0,0 +1,20 @@
from django.contrib import admin
import nested_admin
from .models import Parent, Child, GrandChild


class GrandChildInline(nested_admin.NestedStackedInline):
model = GrandChild
extra = 0
min_num = 1


class ChildInline(nested_admin.NestedStackedInline):
model = Child
inlines = [GrandChildInline]
extra = 0


@admin.register(Parent)
class ParentAdmin(nested_admin.NestedModelAdmin):
inlines = [ChildInline]
39 changes: 39 additions & 0 deletions nested_admin/tests/nested_delete_validationerrors/models.py
@@ -0,0 +1,39 @@
from __future__ import unicode_literals

from django.db import models
from django.db.models import ForeignKey, CASCADE
from nested_admin.tests.compat import python_2_unicode_compatible


@python_2_unicode_compatible
class Parent(models.Model):
name = models.CharField(max_length=128)

def __str__(self):
return self.name


@python_2_unicode_compatible
class Child(models.Model):
name = models.CharField(max_length=128)
parent = ForeignKey(Parent, on_delete=CASCADE, related_name='children')
position = models.PositiveIntegerField()

class Meta:
ordering = ['position']

def __str__(self):
return self.name


@python_2_unicode_compatible
class GrandChild(models.Model):
name = models.CharField(max_length=128)
parent = ForeignKey(Child, on_delete=CASCADE, related_name='children')
position = models.PositiveIntegerField()

class Meta:
ordering = ['position']

def __str__(self):
return self.name
37 changes: 37 additions & 0 deletions nested_admin/tests/nested_delete_validationerrors/tests.py
@@ -0,0 +1,37 @@
import time
from unittest import SkipTest

from django.conf import settings

from nested_admin.tests.base import BaseNestedAdminTestCase
from .models import Parent, Child, GrandChild


class TestDeleteNestedInlineMinNumRequirement(BaseNestedAdminTestCase):

root_model = Parent
nested_models = (Child, GrandChild)

def test_min_num_delete_bug(self):
"""It should be possible to delete inlines, even if min_num requirement not met"""
rhea = Parent.objects.create(name='Rhea')
poseidon = Child.objects.create(name='Poseidon', parent=rhea, position=0)
zeus = Child.objects.create(name='Zeus', parent=rhea, position=1)
demeter = Child.objects.create(name='Demeter', parent=rhea, position=2)

GrandChild.objects.create(name='Apollo', parent=zeus, position=0)
GrandChild.objects.create(name='Persephone', parent=demeter, position=0)

self.load_admin(rhea)
self.delete_inline([0])
self.save_form()

validation_errors = self.selenium.execute_script(
"return $('ul.errorlist li').length")

self.assertEqual(
0, validation_errors, "Save should have completed without validation errors")

children = Child.objects.filter(parent=rhea)
self.assertNotEqual(3, len(children), "Child with empty grandchild was not deleted")
self.assertEqual(2, len(children))

0 comments on commit 2a3e1a5

Please sign in to comment.