diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 50e7227cd0..5f150aca2b 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1342,9 +1342,8 @@ class ModelAdmin(BaseModelAdmin): 'name': force_text(opts.verbose_name), 'key': escape(object_id)}) if request.method == 'POST' and "_saveasnew" in request.POST: - return self.add_view(request, form_url=reverse('admin:%s_%s_add' % ( - opts.app_label, opts.model_name), - current_app=self.admin_site.name)) + object_id = None + obj = None ModelForm = self.get_form(request, obj) if request.method == 'POST': @@ -1366,6 +1365,8 @@ class ModelAdmin(BaseModelAdmin): else: self.log_change(request, new_object, change_message) return self.response_change(request, new_object) + else: + form_validated = False else: if add: initial = self.get_changeform_initial_data(request) @@ -1401,6 +1402,12 @@ class ModelAdmin(BaseModelAdmin): preserved_filters=self.get_preserved_filters(request), ) + # Hide the "Save" and "Save and continue" buttons if "Save as New" was + # previously chosen to prevent the interface from getting confusing. + if request.method == 'POST' and not form_validated and "_saveasnew" in request.POST: + context['show_save'] = False + context['show_save_and_continue'] = False + context.update(extra_context or {}) return self.render_change_form(request, context, add=add, change=not add, obj=obj, form_url=form_url) diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 64fb4ebce7..30f9ac4a15 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -30,6 +30,8 @@ def submit_row(context): change = context['change'] is_popup = context['is_popup'] save_as = context['save_as'] + show_save = context.get('show_save', True) + show_save_and_continue = context.get('show_save_and_continue', True) ctx = { 'opts': opts, 'show_delete_link': ( @@ -41,9 +43,9 @@ def submit_row(context): context['has_add_permission'] and not is_popup and (not save_as or context['add']) ), - 'show_save_and_continue': not is_popup and context['has_change_permission'], + 'show_save_and_continue': not is_popup and context['has_change_permission'] and show_save_and_continue, 'is_popup': is_popup, - 'show_save': True, + 'show_save': show_save, 'preserved_filters': context.get('preserved_filters'), } if context.get('original') is not None: diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index d070e5a55e..aa040138e0 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -288,6 +288,7 @@ class ChildInline(admin.StackedInline): class ParentAdmin(admin.ModelAdmin): model = Parent inlines = [ChildInline] + save_as = True list_editable = ('name',) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 845d3d9ee2..0be371fa7c 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -10,6 +10,7 @@ from django.contrib.contenttypes.fields import ( GenericForeignKey, GenericRelation, ) from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.core.files.storage import FileSystemStorage from django.db import models from django.utils.encoding import python_2_unicode_compatible @@ -311,11 +312,19 @@ class Vodcast(Media): class Parent(models.Model): name = models.CharField(max_length=128) + def clean(self): + if self.name == '_invalid': + raise ValidationError('invalid') + class Child(models.Model): parent = models.ForeignKey(Parent, editable=False) name = models.CharField(max_length=30, blank=True) + def clean(self): + if self.name == '_invalid': + raise ValidationError('invalid') + @python_2_unicode_compatible class EmptyModel(models.Model): diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 8ead8ee93c..93c3a7a710 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -1151,18 +1151,59 @@ class SaveAsTests(TestCase): self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=self.per1.pk)), 1) - def test_save_as_display(self): + def test_save_as_new_with_validation_errors(self): """ - Ensure that 'save as' is displayed when activated and after submitting - invalid data aside save_as_new will not show us a form to overwrite the - initial model. + Ensure that when you click "Save as new" and have a validation error, + you only see the "Save as new" button and not the other save buttons, + and that only the "Save as" button is visible. """ - change_url = reverse('admin:admin_views_person_change', args=(self.per1.pk,)) - response = self.client.get(change_url) - self.assertTrue(response.context['save_as']) - post_data = {'_saveasnew': '', 'name': 'John M', 'gender': 3, 'alive': 'checked'} - response = self.client.post(change_url, post_data) - self.assertEqual(response.context['form_url'], reverse('admin:admin_views_person_add')) + response = self.client.post(reverse('admin:admin_views_person_change', args=(self.per1.pk,)), { + '_saveasnew': '', + 'gender': 'invalid', + '_addanother': 'fail', + }) + self.assertContains(response, 'Please correct the errors below.') + self.assertFalse(response.context['show_save_and_add_another']) + self.assertFalse(response.context['show_save_and_continue']) + self.assertTrue(response.context['show_save_as_new']) + + def test_save_as_new_with_validation_errors_with_inlines(self): + parent = Parent.objects.create(name='Father') + child = Child.objects.create(parent=parent, name='Child') + response = self.client.post(reverse('admin:admin_views_parent_change', args=(parent.pk,)), { + '_saveasnew': 'Save as new', + 'child_set-0-parent': parent.pk, + 'child_set-0-id': child.pk, + 'child_set-0-name': 'Child', + 'child_set-INITIAL_FORMS': 1, + 'child_set-MAX_NUM_FORMS': 1000, + 'child_set-MIN_NUM_FORMS': 0, + 'child_set-TOTAL_FORMS': 4, + 'name': '_invalid', + }) + self.assertContains(response, 'Please correct the error below.') + self.assertFalse(response.context['show_save_and_add_another']) + self.assertFalse(response.context['show_save_and_continue']) + self.assertTrue(response.context['show_save_as_new']) + + def test_save_as_new_with_inlines_with_validation_errors(self): + parent = Parent.objects.create(name='Father') + child = Child.objects.create(parent=parent, name='Child') + response = self.client.post(reverse('admin:admin_views_parent_change', args=(parent.pk,)), { + '_saveasnew': 'Save as new', + 'child_set-0-parent': parent.pk, + 'child_set-0-id': child.pk, + 'child_set-0-name': '_invalid', + 'child_set-INITIAL_FORMS': 1, + 'child_set-MAX_NUM_FORMS': 1000, + 'child_set-MIN_NUM_FORMS': 0, + 'child_set-TOTAL_FORMS': 4, + 'name': 'Father', + }) + self.assertContains(response, 'Please correct the error below.') + self.assertFalse(response.context['show_save_and_add_another']) + self.assertFalse(response.context['show_save_and_continue']) + self.assertTrue(response.context['show_save_as_new']) @override_settings(ROOT_URLCONF="admin_views.urls")