From 35d1cd0b28d1d9cd7bffbfbc6cc2e02b58404415 Mon Sep 17 00:00:00 2001
From: Julien Phalip <jphalip@gmail.com>
Date: Sat, 22 Dec 2012 20:00:08 +0100
Subject: [PATCH] Fixed #19505 -- A more flexible implementation for
 customizable admin redirect urls.

Work by Julien Phalip.

Refs #8001, #18310, #19505. See also 0b908b92a2ca4fb74a103e96bb75c53c05d0a428.
---
 django/contrib/admin/options.py               | 165 ++++++------------
 django/contrib/auth/admin.py                  |   5 +-
 docs/internals/deprecation.txt                |   6 +
 .../admin_custom_urls/models.py               |  63 ++++---
 .../admin_custom_urls/tests.py                |  79 +++++----
 5 files changed, 144 insertions(+), 174 deletions(-)

diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 1827d40159..fa6d288f58 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -9,7 +9,7 @@ from django.forms.models import (modelform_factory, modelformset_factory,
     inlineformset_factory, BaseInlineFormSet)
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.admin import widgets, helpers
-from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
+from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
 from django.contrib.admin.templatetags.admin_static import static
 from django.contrib import messages
 from django.views.decorators.csrf import csrf_protect
@@ -38,6 +38,7 @@ HORIZONTAL, VERTICAL = 1, 2
 # returns the <ul> class for a given radio_admin field
 get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
 
+
 class IncorrectLookupParameters(Exception):
     pass
 
@@ -62,6 +63,7 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = {
 
 csrf_protect_m = method_decorator(csrf_protect)
 
+
 class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
     """Functionality common to both ModelAdmin and InlineAdmin."""
 
@@ -150,7 +152,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
                 })
             if 'choices' not in kwargs:
                 kwargs['choices'] = db_field.get_choices(
-                    include_blank = db_field.blank,
+                    include_blank=db_field.blank,
                     blank_choice=[('', _('None'))]
                 )
         return db_field.formfield(**kwargs)
@@ -787,49 +789,37 @@ class ModelAdmin(BaseModelAdmin):
             "admin/change_form.html"
         ], context, current_app=self.admin_site.name)
 
-    def response_add(self, request, obj, post_url_continue='../%s/',
-                     continue_editing_url=None, add_another_url=None,
-                     hasperm_url=None, noperm_url=None):
+    def response_add(self, request, obj, post_url_continue=None):
         """
         Determines the HttpResponse for the add_view stage.
-
-        :param request: HttpRequest instance.
-        :param obj: Object just added.
-        :param post_url_continue: Deprecated/undocumented.
-        :param continue_editing_url: URL where user will be redirected after
-                                     pressing 'Save and continue editing'.
-        :param add_another_url: URL where user will be redirected after
-                                pressing 'Save and add another'.
-        :param hasperm_url: URL to redirect after a successful object creation
-                            when the user has change permissions.
-        :param noperm_url: URL to redirect after a successful object creation
-                           when the user has no change permissions.
         """
-        if post_url_continue != '../%s/':
-            warnings.warn("The undocumented 'post_url_continue' argument to "
-                          "ModelAdmin.response_add() is deprecated, use the new "
-                          "*_url arguments instead.", DeprecationWarning,
-                          stacklevel=2)
         opts = obj._meta
-        pk_value = obj.pk
-        app_label = opts.app_label
-        model_name = opts.module_name
-        site_name = self.admin_site.name
+        pk_value = obj._get_pk_val()
 
         msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
-
         # Here, we distinguish between different save types by checking for
         # the presence of keys in request.POST.
         if "_continue" in request.POST:
             msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
             self.message_user(request, msg)
-            if continue_editing_url is None:
-                continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name)
-            url = reverse(continue_editing_url, args=(quote(pk_value),),
-                          current_app=site_name)
+            if post_url_continue is None:
+                post_url_continue = reverse('admin:%s_%s_change' %
+                                            (opts.app_label, opts.module_name),
+                                            args=(pk_value,),
+                                            current_app=self.admin_site.name)
+            else:
+                try:
+                    post_url_continue = post_url_continue % pk_value
+                    warnings.warn(
+                        "The use of string formats for post_url_continue "
+                        "in ModelAdmin.response_add() is deprecated. Provide "
+                        "a pre-formatted url instead.",
+                        DeprecationWarning, stacklevel=2)
+                except TypeError:
+                    pass
             if "_popup" in request.POST:
-                url += "?_popup=1"
-            return HttpResponseRedirect(url)
+                post_url_continue += "?_popup=1"
+            return HttpResponseRedirect(post_url_continue)
 
         if "_popup" in request.POST:
             return HttpResponse(
@@ -840,102 +830,61 @@ class ModelAdmin(BaseModelAdmin):
         elif "_addanother" in request.POST:
             msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
             self.message_user(request, msg)
-            if add_another_url is None:
-                add_another_url = 'admin:%s_%s_add' % (app_label, model_name)
-            url = reverse(add_another_url, current_app=site_name)
-            return HttpResponseRedirect(url)
+            return HttpResponseRedirect(request.path)
         else:
             msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
             self.message_user(request, msg)
+            return self.response_post_save(request, obj)
 
-            # Figure out where to redirect. If the user has change permission,
-            # redirect to the change-list page for this object. Otherwise,
-            # redirect to the admin index.
-            if self.has_change_permission(request, None):
-                if hasperm_url is None:
-                    hasperm_url = 'admin:%s_%s_changelist' % (app_label, model_name)
-                url = reverse(hasperm_url, current_app=site_name)
-            else:
-                if noperm_url is None:
-                    noperm_url = 'admin:index'
-                url = reverse(noperm_url, current_app=site_name)
-            return HttpResponseRedirect(url)
-
-    def response_change(self, request, obj, continue_editing_url=None,
-                        save_as_new_url=None, add_another_url=None,
-                        hasperm_url=None, noperm_url=None):
+    def response_change(self, request, obj):
         """
         Determines the HttpResponse for the change_view stage.
-
-        :param request: HttpRequest instance.
-        :param obj: Object just modified.
-        :param continue_editing_url: URL where user will be redirected after
-                                     pressing 'Save and continue editing'.
-        :param save_as_new_url: URL where user will be redirected after pressing
-                                'Save as new' (when applicable).
-        :param add_another_url: URL where user will be redirected after pressing
-                                'Save and add another'.
-        :param hasperm_url: URL to redirect after a successful object edition when
-                            the user has change permissions.
-        :param noperm_url: URL to redirect after a successful object edition when
-                           the user has no change permissions.
         """
-        opts = obj._meta
+        opts = self.model._meta
 
-        app_label = opts.app_label
-        model_name = opts.module_name
-        site_name = self.admin_site.name
-        verbose_name = opts.verbose_name
-        # Handle proxy models automatically created by .only() or .defer().
-        # Refs #14529
-        if obj._deferred:
-            opts_ = opts.proxy_for_model._meta
-            verbose_name = opts_.verbose_name
-            model_name = opts_.module_name
-
-        msg_dict = {'name': force_text(verbose_name), 'obj': force_text(obj)}
+        pk_value = obj._get_pk_val()
 
+        msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
         if "_continue" in request.POST:
             msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
             self.message_user(request, msg)
-            if continue_editing_url is None:
-                continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name)
-            url = reverse(continue_editing_url, args=(quote(obj.pk),),
-                          current_app=site_name)
-            if "_popup" in request.POST:
-                url += "?_popup=1"
-            return HttpResponseRedirect(url)
+            if "_popup" in request.REQUEST:
+                return HttpResponseRedirect(request.path + "?_popup=1")
+            else:
+                return HttpResponseRedirect(request.path)
         elif "_saveasnew" in request.POST:
             msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
             self.message_user(request, msg)
-            if save_as_new_url is None:
-                save_as_new_url = 'admin:%s_%s_change' % (app_label, model_name)
-            url = reverse(save_as_new_url, args=(quote(obj.pk),),
-                          current_app=site_name)
-            return HttpResponseRedirect(url)
+            return HttpResponseRedirect(reverse('admin:%s_%s_change' %
+                                        (opts.app_label, opts.module_name),
+                                        args=(pk_value,),
+                                        current_app=self.admin_site.name))
         elif "_addanother" in request.POST:
             msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
             self.message_user(request, msg)
-            if add_another_url is None:
-                add_another_url = 'admin:%s_%s_add' % (app_label, model_name)
-            url = reverse(add_another_url, current_app=site_name)
-            return HttpResponseRedirect(url)
+            return HttpResponseRedirect(reverse('admin:%s_%s_add' %
+                                        (opts.app_label, opts.module_name),
+                                        current_app=self.admin_site.name))
         else:
             msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
             self.message_user(request, msg)
-            # Figure out where to redirect. If the user has change permission,
-            # redirect to the change-list page for this object. Otherwise,
-            # redirect to the admin index.
-            if self.has_change_permission(request, None):
-                if hasperm_url is None:
-                    hasperm_url = 'admin:%s_%s_changelist' % (app_label,
-                                                              model_name)
-                url = reverse(hasperm_url, current_app=site_name)
-            else:
-                if noperm_url is None:
-                    noperm_url = 'admin:index'
-                url = reverse(noperm_url, current_app=site_name)
-            return HttpResponseRedirect(url)
+            return self.response_post_save(request, obj)
+
+    def response_post_save(self, request, obj):
+        """
+        Figure out where to redirect after the 'Save' button has been pressed.
+        If the user has change permission, redirect to the change-list page for
+        this object. Otherwise, redirect to the admin index.
+        """
+        opts = self.model._meta
+        if self.has_change_permission(request, None):
+            post_url = reverse('admin:%s_%s_changelist' %
+                               (opts.app_label, opts.module_name),
+                               current_app=self.admin_site.name)
+        else:
+            post_url = reverse('admin:index',
+                               current_app=self.admin_site.name)
+        return HttpResponseRedirect(post_url)
 
     def response_action(self, request, queryset):
         """
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index d15a387a7e..7b816674d3 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -153,7 +153,7 @@ class UserAdmin(admin.ModelAdmin):
             'admin/auth/user/change_password.html',
             context, current_app=self.admin_site.name)
 
-    def response_add(self, request, obj, **kwargs):
+    def response_add(self, request, obj, post_url_continue=None):
         """
         Determines the HttpResponse for the add_view stage. It mostly defers to
         its superclass implementation but is customized because the User model
@@ -166,7 +166,8 @@ class UserAdmin(admin.ModelAdmin):
         # * We are adding a user in a popup
         if '_addanother' not in request.POST and '_popup' not in request.POST:
             request.POST['_continue'] = 1
-        return super(UserAdmin, self).response_add(request, obj, **kwargs)
+        return super(UserAdmin, self).response_add(request, obj,
+                                                   post_url_continue)
 
 admin.site.register(Group, GroupAdmin)
 admin.site.register(User, UserAdmin)
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 414da30ff8..386dfc0b00 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -268,6 +268,12 @@ these changes.
 * ``django.contrib.markup`` will be removed following an accelerated
   deprecation.
 
+* The value for the ``post_url_continue`` parameter in
+  ``ModelAdmin.response_add()`` will have to be either ``None`` (to redirect
+  to the newly created object's edit page) or a pre-formatted url. String
+  formats, such as the previous default ``'../%s/'``, will not be accepted any
+  more.
+
 1.7
 ---
 
diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py
index b9b3285463..cc8e730c26 100644
--- a/tests/regressiontests/admin_custom_urls/models.py
+++ b/tests/regressiontests/admin_custom_urls/models.py
@@ -1,7 +1,9 @@
 from functools import update_wrapper
 
 from django.contrib import admin
+from django.core.urlresolvers import reverse
 from django.db import models
+from django.http import HttpResponseRedirect
 from django.utils.encoding import python_2_unicode_compatible
 
 
@@ -49,41 +51,38 @@ class ActionAdmin(admin.ModelAdmin):
         ) + self.remove_url(view_name)
 
 
-admin.site.register(Action, ActionAdmin)
-
-
 class Person(models.Model):
-    nick = models.CharField(max_length=20)
-
-
-class PersonAdmin(admin.ModelAdmin):
-    """A custom ModelAdmin that customizes the deprecated post_url_continue
-    argument to response_add()"""
-    def response_add(self, request, obj, post_url_continue='../%s/continue/',
-                     continue_url=None, add_url=None, hasperm_url=None,
-                     noperm_url=None):
-        return super(PersonAdmin, self).response_add(request, obj,
-                                                     post_url_continue,
-                                                     continue_url, add_url,
-                                                     hasperm_url, noperm_url)
-
-
-admin.site.register(Person, PersonAdmin)
-
-
-class City(models.Model):
     name = models.CharField(max_length=20)
 
+class PersonAdmin(admin.ModelAdmin):
 
-class CityAdmin(admin.ModelAdmin):
-    """A custom ModelAdmin that redirects to the changelist when the user
-    presses the 'Save and add another' button when adding a model instance."""
-    def response_add(self, request, obj,
-                     add_another_url='admin:admin_custom_urls_city_changelist',
-                     **kwargs):
-        return super(CityAdmin, self).response_add(request, obj,
-                                                   add_another_url=add_another_url,
-                                                   **kwargs)
+    def response_post_save(self, request, obj):
+        return HttpResponseRedirect(
+            reverse('admin:admin_custom_urls_person_history', args=[obj.pk]))
 
 
-admin.site.register(City, CityAdmin)
+class Car(models.Model):
+    name = models.CharField(max_length=20)
+
+class CarAdmin(admin.ModelAdmin):
+
+    def response_add(self, request, obj, post_url_continue=None):
+        return super(CarAdmin, self).response_add(
+            request, obj, post_url_continue=reverse('admin:admin_custom_urls_car_history', args=[obj.pk]))
+
+
+class CarDeprecated(models.Model):
+    """ This class must be removed in Django 1.6 """
+    name = models.CharField(max_length=20)
+
+class CarDeprecatedAdmin(admin.ModelAdmin):
+    """ This class must be removed in Django 1.6 """
+    def response_add(self, request, obj, post_url_continue=None):
+        return super(CarDeprecatedAdmin, self).response_add(
+            request, obj, post_url_continue='../%s/history/')
+
+
+admin.site.register(Action, ActionAdmin)
+admin.site.register(Person, PersonAdmin)
+admin.site.register(Car, CarAdmin)
+admin.site.register(CarDeprecated, CarDeprecatedAdmin)
\ No newline at end of file
diff --git a/tests/regressiontests/admin_custom_urls/tests.py b/tests/regressiontests/admin_custom_urls/tests.py
index 87c72e2e71..d691a97557 100644
--- a/tests/regressiontests/admin_custom_urls/tests.py
+++ b/tests/regressiontests/admin_custom_urls/tests.py
@@ -1,5 +1,4 @@
 from __future__ import absolute_import, unicode_literals
-
 import warnings
 
 from django.contrib.admin.util import quote
@@ -8,7 +7,7 @@ from django.template.response import TemplateResponse
 from django.test import TestCase
 from django.test.utils import override_settings
 
-from .models import Action, Person, City
+from .models import Action, Person, Car, CarDeprecated
 
 
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -86,8 +85,8 @@ class AdminCustomUrlsTest(TestCase):
 
 
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
-class CustomUrlsWorkflowTests(TestCase):
-    fixtures = ['users.json']
+class CustomRedirects(TestCase):
+    fixtures = ['users.json', 'actions.json']
 
     def setUp(self):
         self.client.login(username='super', password='secret')
@@ -95,33 +94,49 @@ class CustomUrlsWorkflowTests(TestCase):
     def tearDown(self):
         self.client.logout()
 
-    def test_old_argument_deprecation(self):
-        """Test reporting of post_url_continue deprecation."""
-        post_data = {
-            'nick': 'johndoe',
-        }
-        cnt = Person.objects.count()
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("always")
-            response = self.client.post(reverse('admin:admin_custom_urls_person_add'), post_data)
-            self.assertEqual(response.status_code, 302)
-            self.assertEqual(Person.objects.count(), cnt + 1)
-            # We should get a DeprecationWarning
-            self.assertEqual(len(w), 1)
-            self.assertTrue(isinstance(w[0].message, DeprecationWarning))
+    def test_post_save_redirect(self):
+        """
+        Ensures that ModelAdmin.response_post_save() controls the redirection
+        after the 'Save' button has been pressed.
+        Refs 8001, 18310, 19505.
+        """
+        post_data = { 'name': 'John Doe', }
+        self.assertEqual(Person.objects.count(), 0)
+        response = self.client.post(
+            reverse('admin:admin_custom_urls_person_add'), post_data)
+        persons = Person.objects.all()
+        self.assertEqual(len(persons), 1)
+        self.assertRedirects(
+            response, reverse('admin:admin_custom_urls_person_history', args=[persons[0].pk]))
 
-    def test_custom_add_another_redirect(self):
-        """Test customizability of post-object-creation redirect URL."""
-        post_data = {
-            'name': 'Rome',
-            '_addanother': '1',
-        }
-        cnt = City.objects.count()
+    def test_post_url_continue(self):
+        """
+        Ensures that the ModelAdmin.response_add()'s parameter `post_url_continue`
+        controls the redirection after an object has been created.
+        """
+        post_data = { 'name': 'SuperFast', '_continue': '1' }
+        self.assertEqual(Car.objects.count(), 0)
+        response = self.client.post(
+            reverse('admin:admin_custom_urls_car_add'), post_data)
+        cars = Car.objects.all()
+        self.assertEqual(len(cars), 1)
+        self.assertRedirects(
+            response, reverse('admin:admin_custom_urls_car_history', args=[cars[0].pk]))
+
+    def test_post_url_continue_string_formats(self):
+        """
+        Ensures that string formats are accepted for post_url_continue. This
+        is a deprecated functionality that will be removed in Django 1.6 along
+        with this test.
+        """
         with warnings.catch_warnings(record=True) as w:
-            # POST to the view whose post-object-creation redir URL argument we
-            # are customizing (object creation)
-            response = self.client.post(reverse('admin:admin_custom_urls_city_add'), post_data)
-            self.assertEqual(City.objects.count(), cnt + 1)
-            # Check that it redirected to the URL we set
-            self.assertRedirects(response, reverse('admin:admin_custom_urls_city_changelist'))
-            self.assertEqual(len(w), 0) # We should get no DeprecationWarning
+            post_data = { 'name': 'SuperFast', '_continue': '1' }
+            self.assertEqual(Car.objects.count(), 0)
+            response = self.client.post(
+                reverse('admin:admin_custom_urls_cardeprecated_add'), post_data)
+            cars = CarDeprecated.objects.all()
+            self.assertEqual(len(cars), 1)
+            self.assertRedirects(
+                response, reverse('admin:admin_custom_urls_cardeprecated_history', args=[cars[0].pk]))
+        self.assertEqual(len(w), 1)
+        self.assertTrue(isinstance(w[0].message, DeprecationWarning))
\ No newline at end of file