Skip to content

Commit

Permalink
Fix #14642 - Admin save as new doesn't work with GenericTabularInline
Browse files Browse the repository at this point in the history
  • Loading branch information
Tomer Chachamu committed Dec 28, 2017
1 parent 47aba5e commit 9093e9b
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 4 deletions.
8 changes: 7 additions & 1 deletion django/contrib/contenttypes/forms.py
Expand Up @@ -9,14 +9,15 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
A formset for generic inline objects to a parent.
"""

def __init__(self, data=None, files=None, instance=None, save_as_new=None,
def __init__(self, data=None, files=None, instance=None, save_as_new=False,
prefix=None, queryset=None, **kwargs):
opts = self.model._meta
self.instance = instance
self.rel_name = (
opts.app_label + '-' + opts.model_name + '-' +
self.ct_field.name + '-' + self.ct_fk_field.name
)
self.save_as_new = save_as_new
if self.instance is None or self.instance.pk is None:
qs = self.model._default_manager.none()
else:
Expand All @@ -29,6 +30,11 @@ def __init__(self, data=None, files=None, instance=None, save_as_new=None,
})
super().__init__(queryset=qs, data=data, files=files, prefix=prefix, **kwargs)

def initial_form_count(self):
if self.save_as_new:
return 0
return super().initial_form_count()

@classmethod
def get_default_prefix(cls):
opts = cls.model._meta
Expand Down
2 changes: 2 additions & 0 deletions tests/generic_inline_admin/admin.py
Expand Up @@ -17,6 +17,8 @@ class EpisodeAdmin(admin.ModelAdmin):
MediaInline,
]

save_as = True


class PhoneNumberInline(GenericTabularInline):
model = PhoneNumber
Expand Down
40 changes: 37 additions & 3 deletions tests/generic_inline_admin/tests.py
Expand Up @@ -72,24 +72,58 @@ def test_basic_add_POST(self):

def test_basic_edit_POST(self):
"""
A smoke test to ensure POST on edit_view works.
A test to ensure POST on edit_view saves changes to the database.
"""
new_mp3_url = "http://example.com/podcast2.mp3"
new_png_url = "http://example.com/logo2.png"
post_data = {
"name": "This Week in Django",
# inline data
"generic_inline_admin-media-content_type-object_id-TOTAL_FORMS": "3",
"generic_inline_admin-media-content_type-object_id-INITIAL_FORMS": "2",
"generic_inline_admin-media-content_type-object_id-MAX_NUM_FORMS": "0",
"generic_inline_admin-media-content_type-object_id-0-id": "%d" % self.mp3_media_pk,
"generic_inline_admin-media-content_type-object_id-0-url": "http://example.com/podcast.mp3",
"generic_inline_admin-media-content_type-object_id-0-url": new_mp3_url,
"generic_inline_admin-media-content_type-object_id-1-id": "%d" % self.png_media_pk,
"generic_inline_admin-media-content_type-object_id-1-url": "http://example.com/logo.png",
"generic_inline_admin-media-content_type-object_id-1-url": new_png_url,
"generic_inline_admin-media-content_type-object_id-2-id": "",
"generic_inline_admin-media-content_type-object_id-2-url": "",
}
url = reverse('admin:generic_inline_admin_episode_change', args=(self.episode_pk,))
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302) # redirect somewhere
self.assertEqual(Media.objects.get(pk=self.mp3_media_pk).url, new_mp3_url)
self.assertEqual(Media.objects.get(pk=self.png_media_pk).url, new_png_url)

def test_basic_edit_POST_as_new(self):
"""
A smoke test to ensure "save as new" saves additional objects to the database.
"""
post_data = {
"name": "Next Week in Django",
"_saveasnew": "Save as New",
# inline data
"generic_inline_admin-media-content_type-object_id-TOTAL_FORMS": "3",
"generic_inline_admin-media-content_type-object_id-INITIAL_FORMS": "2",
"generic_inline_admin-media-content_type-object_id-MAX_NUM_FORMS": "0",
"generic_inline_admin-media-content_type-object_id-0-id": "%d" % self.mp3_media_pk,
"generic_inline_admin-media-content_type-object_id-0-url": 'http://example.com/podcast.mp3',
"generic_inline_admin-media-content_type-object_id-1-id": "%d" % self.png_media_pk,
"generic_inline_admin-media-content_type-object_id-1-url": 'http://example.com/logo.png',
"generic_inline_admin-media-content_type-object_id-2-id": "",
"generic_inline_admin-media-content_type-object_id-2-url": "",
}
url = reverse('admin:generic_inline_admin_episode_change', args=(self.episode_pk,))
response = self.client.post(url, post_data)
self.assertEqual(response.status_code, 302) # redirect somewhere
new_episode = Episode.objects.latest('pk')
new_media_urls = Media.objects.filter(content_type=ContentType.objects.get_for_model(Episode),
object_id=new_episode.pk).values_list('url', flat=True)
self.assertSequenceEqual(sorted(new_media_urls), [
'http://example.com/logo.png',
'http://example.com/podcast.mp3',
])
self.assertEqual(Media.objects.filter(url='http://example.com/podcast.mp3').count(), 2)

def test_generic_inline_formset(self):
EpisodeMediaFormSet = generic_inlineformset_factory(
Expand Down
50 changes: 50 additions & 0 deletions tests/generic_relations/tests.py
Expand Up @@ -4,6 +4,7 @@
from django.core.exceptions import FieldError
from django.db import IntegrityError, models
from django.db.models import Q
from django.http import QueryDict
from django.test import SimpleTestCase, TestCase
from django.test.utils import isolate_apps

Expand Down Expand Up @@ -513,6 +514,55 @@ def test_subclasses_with_parent_gen_rel(self):
TaggedItem.objects.create(content_object=bear, tag='orange')
self.assertEqual(Carrot.objects.get(tags__tag='orange'), bear)

def test_generic_inline_formsets_initial_count(self):
"""
Test for initial form count considering save_as_new.
"""
quartz = Mineral.objects.create(name="Quartz", hardness=7)

GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)
data = {
'form-TOTAL_FORMS': '3',
'form-INITIAL_FORMS': '3',
'form-MAX_NUM_FORMS': '',
}
formset = GenericFormSet(data=data, prefix='form')
self.assertEqual(formset.initial_form_count(), 3)
formset = GenericFormSet(data=data, prefix='form', save_as_new=True)
self.assertEqual(formset.initial_form_count(), 0)

def test_generic_inline_formsets_save_as_new(self):
# The save_as_new parameter lets you re-associate the data to a new
# instance. This is used in the admin for save_as functionality.
quartz = Mineral.objects.create(name="Quartz", hardness=7)
GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1)

# An immutable QueryDict simulates request.POST.
data = QueryDict(mutable=True)
data.update({
'generic_relations-taggeditem-content_type-object_id-TOTAL_FORMS': '3',
'generic_relations-taggeditem-content_type-object_id-INITIAL_FORMS': '2',
'generic_relations-taggeditem-content_type-object_id-MAX_NUM_FORMS': '',
'generic_relations-taggeditem-content_type-object_id-0-id': '1',
'generic_relations-taggeditem-content_type-object_id-0-tag': 'Tough',
'generic_relations-taggeditem-content_type-object_id-1-id': '2',
'generic_relations-taggeditem-content_type-object_id-1-tag': 'Shiny',
})
data._mutable = False

formset = GenericFormSet(data, instance=quartz, save_as_new=True)
self.assertTrue(formset.is_valid())
self.assertIs(data._mutable, False)

new_mineral = Mineral.objects.create(name="Ruby", hardness=7)
formset = GenericFormSet(data, instance=new_mineral, save_as_new=True)
self.assertTrue(formset.is_valid())
saved = formset.save()
self.assertEqual(len(saved), 2)
tag1, tag2 = saved
self.assertEqual(tag1.tag, 'Tough')
self.assertEqual(tag2.tag, 'Shiny')

def test_generic_inline_formsets_initial(self):
"""
Test for #17927 Initial values support for BaseGenericInlineFormSet.
Expand Down

0 comments on commit 9093e9b

Please sign in to comment.