Kaydet (Commit) 3ea0c7d3 authored tarafından Javier Mansilla's avatar Javier Mansilla Kaydeden (comit) Ramiro Morales

Fixed #19838 -- Admin: Don't leak a 500 HTTP status when trying to delete protected FKs.

Thanks rafadev for the report and Javier Mansilla for the fix.
üst 51cc7029
...@@ -363,6 +363,7 @@ answer newbie questions, and generally made Django that much better: ...@@ -363,6 +363,7 @@ answer newbie questions, and generally made Django that much better:
Mike Malone <mjmalone@gmail.com> Mike Malone <mjmalone@gmail.com>
Martin Maney <http://www.chipy.org/Martin_Maney> Martin Maney <http://www.chipy.org/Martin_Maney>
Michael Manfre <mmanfre@gmail.com> Michael Manfre <mmanfre@gmail.com>
Javier Mansilla <javimansilla@gmail.com>
masonsimon+django@gmail.com masonsimon+django@gmail.com
Manuzhai Manuzhai
Petr Marhoun <petr.marhoun@gmail.com> Petr Marhoun <petr.marhoun@gmail.com>
......
...@@ -3,12 +3,13 @@ from functools import update_wrapper, partial ...@@ -3,12 +3,13 @@ from functools import update_wrapper, partial
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.forms.formsets import all_valid from django.forms.formsets import all_valid, DELETION_FIELD_NAME
from django.forms.models import (modelform_factory, modelformset_factory, from django.forms.models import (modelform_factory, modelformset_factory,
inlineformset_factory, BaseInlineFormSet) inlineformset_factory, BaseInlineFormSet)
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets, helpers from django.contrib.admin import widgets, helpers
from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects,
model_format_dict, NestedObjects)
from django.contrib.admin.templatetags.admin_static import static from django.contrib.admin.templatetags.admin_static import static
from django.contrib import messages from django.contrib import messages
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
...@@ -1452,7 +1453,44 @@ class InlineModelAdmin(BaseModelAdmin): ...@@ -1452,7 +1453,44 @@ class InlineModelAdmin(BaseModelAdmin):
"max_num": self.max_num, "max_num": self.max_num,
"can_delete": can_delete, "can_delete": can_delete,
} }
defaults.update(kwargs) defaults.update(kwargs)
base_model_form = defaults['form']
class DeleteProtectedModelForm(base_model_form):
def hand_clean_DELETE(self):
"""
We don't validate the 'DELETE' field itself because on
templates it's not rendered using the field information, but
just using a generic "deletion_field" of the InlineModelAdmin.
"""
if self.cleaned_data.get(DELETION_FIELD_NAME, False):
using = router.db_for_write(self._meta.model)
collector = NestedObjects(using=using)
collector.collect([self.instance])
if collector.protected:
objs = []
for p in collector.protected:
objs.append(
# Translators: Model verbose name and instance representation, suitable to be an item in a list
_('%(class_name)s %(instance)s') % {
'class_name': p._meta.verbose_name,
'instance': p}
)
msg_dict = {'class_name': self._meta.model._meta.verbose_name,
'instance': self.instance,
'related_objects': get_text_list(objs, _('and'))}
msg = _("Deleting %(class_name)s %(instance)s would require "
"deleting the following protected related objects: "
"%(related_objects)s") % msg_dict
raise ValidationError(msg)
def is_valid(self):
result = super(DeleteProtectedModelForm, self).is_valid()
self.hand_clean_DELETE()
return result
defaults['form'] = DeleteProtectedModelForm
return inlineformset_factory(self.parent_model, self.model, **defaults) return inlineformset_factory(self.parent_model, self.model, **defaults)
def get_fieldsets(self, request, obj=None): def get_fieldsets(self, request, obj=None):
......
...@@ -128,8 +128,16 @@ class Novel(models.Model): ...@@ -128,8 +128,16 @@ class Novel(models.Model):
name = models.CharField(max_length=40) name = models.CharField(max_length=40)
class Chapter(models.Model): class Chapter(models.Model):
name = models.CharField(max_length=40)
novel = models.ForeignKey(Novel) novel = models.ForeignKey(Novel)
class FootNote(models.Model):
"""
Model added for ticket 19838
"""
chapter = models.ForeignKey(Chapter, on_delete=models.PROTECT)
note = models.CharField(max_length=40)
# Models for #16838 # Models for #16838
class CapoFamiglia(models.Model): class CapoFamiglia(models.Model):
......
...@@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site ...@@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site
from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2,
Sighting, Title) Sighting, Title, Novel, Chapter, FootNote)
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
...@@ -250,6 +250,44 @@ class TestInlineAdminForm(TestCase): ...@@ -250,6 +250,44 @@ class TestInlineAdminForm(TestCase):
parent_ct = ContentType.objects.get_for_model(Parent) parent_ct = ContentType.objects.get_for_model(Parent)
self.assertEqual(iaf.original.content_type, parent_ct) self.assertEqual(iaf.original.content_type, parent_ct)
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class TestInlineProtectedOnDelete(TestCase):
urls = "admin_inlines.urls"
fixtures = ['admin-views-users.xml']
def setUp(self):
result = self.client.login(username='super', password='secret')
self.assertEqual(result, True)
def tearDown(self):
self.client.logout()
def test_deleting_inline_with_protected_delete_does_not_validate(self):
lotr = Novel.objects.create(name='Lord of the rings')
chapter = Chapter.objects.create(novel=lotr, name='Many Meetings')
foot_note = FootNote.objects.create(chapter=chapter, note='yadda yadda')
change_url = '/admin/admin_inlines/novel/%i/' % lotr.id
response = self.client.get(change_url)
data = {
'name': lotr.name,
'chapter_set-TOTAL_FORMS': 1,
'chapter_set-INITIAL_FORMS': 1,
'chapter_set-MAX_NUM_FORMS': 1000,
'_save': 'Save',
'chapter_set-0-id': chapter.id,
'chapter_set-0-name': chapter.name,
'chapter_set-0-novel': lotr.id,
'chapter_set-0-DELETE': 'on'
}
response = self.client.post(change_url, data)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Deleting chapter %s would require deleting "
"the following protected related objects: foot note %s"
% (chapter, foot_note))
class TestInlinePermissions(TestCase): class TestInlinePermissions(TestCase):
""" """
Make sure the admin respects permissions for objects that are edited Make sure the admin respects permissions for objects that are edited
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment