Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #19838 -- Admin: Don't leak a 500 HTTP status when trying to de…

…lete protected FKs.

Thanks rafadev for the report and Javier Mansilla for the fix.
  • Loading branch information...
commit 3ea0c7d35ac461cb469170486683d10732eb534b 1 parent 51cc702
Javier Mansilla authored February 23, 2013 ramiro committed March 04, 2013
1  AUTHORS
@@ -363,6 +363,7 @@ answer newbie questions, and generally made Django that much better:
363 363
     Mike Malone <mjmalone@gmail.com>
364 364
     Martin Maney <http://www.chipy.org/Martin_Maney>
365 365
     Michael Manfre <mmanfre@gmail.com>
  366
+    Javier Mansilla <javimansilla@gmail.com>
366 367
     masonsimon+django@gmail.com
367 368
     Manuzhai
368 369
     Petr Marhoun <petr.marhoun@gmail.com>
42  django/contrib/admin/options.py
@@ -3,12 +3,13 @@
3 3
 
4 4
 from django import forms
5 5
 from django.conf import settings
6  
-from django.forms.formsets import all_valid
  6
+from django.forms.formsets import all_valid, DELETION_FIELD_NAME
7 7
 from django.forms.models import (modelform_factory, modelformset_factory,
8 8
     inlineformset_factory, BaseInlineFormSet)
9 9
 from django.contrib.contenttypes.models import ContentType
10 10
 from django.contrib.admin import widgets, helpers
11  
-from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
  11
+from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects,
  12
+    model_format_dict, NestedObjects)
12 13
 from django.contrib.admin.templatetags.admin_static import static
13 14
 from django.contrib import messages
14 15
 from django.views.decorators.csrf import csrf_protect
@@ -1452,7 +1453,44 @@ def get_formset(self, request, obj=None, **kwargs):
1452 1453
             "max_num": self.max_num,
1453 1454
             "can_delete": can_delete,
1454 1455
         }
  1456
+
1455 1457
         defaults.update(kwargs)
  1458
+        base_model_form = defaults['form']
  1459
+
  1460
+        class DeleteProtectedModelForm(base_model_form):
  1461
+            def hand_clean_DELETE(self):
  1462
+                """
  1463
+                We don't validate the 'DELETE' field itself because on
  1464
+                templates it's not rendered using the field information, but
  1465
+                just using a generic "deletion_field" of the InlineModelAdmin.
  1466
+                """
  1467
+                if self.cleaned_data.get(DELETION_FIELD_NAME, False):
  1468
+                    using = router.db_for_write(self._meta.model)
  1469
+                    collector = NestedObjects(using=using)
  1470
+                    collector.collect([self.instance])
  1471
+                    if collector.protected:
  1472
+                        objs = []
  1473
+                        for p in collector.protected:
  1474
+                            objs.append(
  1475
+                                # Translators: Model verbose name and instance representation, suitable to be an item in a list
  1476
+                                _('%(class_name)s %(instance)s') % {
  1477
+                                    'class_name': p._meta.verbose_name,
  1478
+                                    'instance': p}
  1479
+                            )
  1480
+                        msg_dict = {'class_name': self._meta.model._meta.verbose_name,
  1481
+                                    'instance': self.instance,
  1482
+                                    'related_objects': get_text_list(objs, _('and'))}
  1483
+                        msg = _("Deleting %(class_name)s %(instance)s would require "
  1484
+                                "deleting the following protected related objects: "
  1485
+                                "%(related_objects)s") % msg_dict
  1486
+                        raise ValidationError(msg)
  1487
+
  1488
+            def is_valid(self):
  1489
+                result = super(DeleteProtectedModelForm, self).is_valid()
  1490
+                self.hand_clean_DELETE()
  1491
+                return result
  1492
+
  1493
+        defaults['form'] = DeleteProtectedModelForm
1456 1494
         return inlineformset_factory(self.parent_model, self.model, **defaults)
1457 1495
 
1458 1496
     def get_fieldsets(self, request, obj=None):
8  tests/admin_inlines/models.py
@@ -128,8 +128,16 @@ class Novel(models.Model):
128 128
     name = models.CharField(max_length=40)
129 129
 
130 130
 class Chapter(models.Model):
  131
+    name = models.CharField(max_length=40)
131 132
     novel = models.ForeignKey(Novel)
132 133
 
  134
+class FootNote(models.Model):
  135
+    """
  136
+    Model added for ticket 19838
  137
+    """
  138
+    chapter = models.ForeignKey(Chapter, on_delete=models.PROTECT)
  139
+    note = models.CharField(max_length=40)
  140
+
133 141
 # Models for #16838
134 142
 
135 143
 class CapoFamiglia(models.Model):
40  tests/admin_inlines/tests.py
@@ -12,7 +12,7 @@
12 12
 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
13 13
     OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
14 14
     ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
15  
-    Sighting, Title)
  15
+    Sighting, Title, Novel, Chapter, FootNote)
16 16
 
17 17
 
18 18
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -250,6 +250,44 @@ def test_immutable_content_type(self):
250 250
         parent_ct = ContentType.objects.get_for_model(Parent)
251 251
         self.assertEqual(iaf.original.content_type, parent_ct)
252 252
 
  253
+
  254
+@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
  255
+class TestInlineProtectedOnDelete(TestCase):
  256
+    urls = "admin_inlines.urls"
  257
+    fixtures = ['admin-views-users.xml']
  258
+
  259
+    def setUp(self):
  260
+        result = self.client.login(username='super', password='secret')
  261
+        self.assertEqual(result, True)
  262
+
  263
+    def tearDown(self):
  264
+        self.client.logout()
  265
+
  266
+    def test_deleting_inline_with_protected_delete_does_not_validate(self):
  267
+        lotr = Novel.objects.create(name='Lord of the rings')
  268
+        chapter = Chapter.objects.create(novel=lotr, name='Many Meetings')
  269
+        foot_note = FootNote.objects.create(chapter=chapter, note='yadda yadda')
  270
+
  271
+        change_url = '/admin/admin_inlines/novel/%i/' % lotr.id
  272
+        response = self.client.get(change_url)
  273
+        data = {
  274
+            'name': lotr.name,
  275
+            'chapter_set-TOTAL_FORMS': 1,
  276
+            'chapter_set-INITIAL_FORMS': 1,
  277
+            'chapter_set-MAX_NUM_FORMS': 1000,
  278
+            '_save': 'Save',
  279
+            'chapter_set-0-id': chapter.id,
  280
+            'chapter_set-0-name': chapter.name,
  281
+            'chapter_set-0-novel': lotr.id,
  282
+            'chapter_set-0-DELETE': 'on'
  283
+        }
  284
+        response = self.client.post(change_url, data)
  285
+        self.assertEqual(response.status_code, 200)
  286
+        self.assertContains(response, "Deleting chapter %s would require deleting "
  287
+                            "the following protected related objects: foot note %s"
  288
+                            % (chapter, foot_note))
  289
+
  290
+
253 291
 class TestInlinePermissions(TestCase):
254 292
     """
255 293
     Make sure the admin respects permissions for objects that are edited

0 notes on commit 3ea0c7d

Please sign in to comment.
Something went wrong with that request. Please try again.