Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #19505 -- A more flexible implementation for customizable admin…

… redirect urls.

Work by Julien Phalip.

Refs #8001, #18310, #19505. See also 0b908b9.
  • Loading branch information...
commit 35d1cd0b28d1d9cd7bffbfbc6cc2e02b58404415 1 parent 4a71b84
Julien Phalip authored December 22, 2012 ramiro committed December 24, 2012
167  django/contrib/admin/options.py
@@ -9,7 +9,7 @@
9 9
     inlineformset_factory, BaseInlineFormSet)
10 10
 from django.contrib.contenttypes.models import ContentType
11 11
 from django.contrib.admin import widgets, helpers
12  
-from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
  12
+from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict
13 13
 from django.contrib.admin.templatetags.admin_static import static
14 14
 from django.contrib import messages
15 15
 from django.views.decorators.csrf import csrf_protect
@@ -38,6 +38,7 @@
38 38
 # returns the <ul> class for a given radio_admin field
39 39
 get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
40 40
 
  41
+
41 42
 class IncorrectLookupParameters(Exception):
42 43
     pass
43 44
 
@@ -62,6 +63,7 @@ class IncorrectLookupParameters(Exception):
62 63
 
63 64
 csrf_protect_m = method_decorator(csrf_protect)
64 65
 
  66
+
65 67
 class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
66 68
     """Functionality common to both ModelAdmin and InlineAdmin."""
67 69
 
@@ -150,7 +152,7 @@ def formfield_for_choice_field(self, db_field, request=None, **kwargs):
150 152
                 })
151 153
             if 'choices' not in kwargs:
152 154
                 kwargs['choices'] = db_field.get_choices(
153  
-                    include_blank = db_field.blank,
  155
+                    include_blank=db_field.blank,
154 156
                     blank_choice=[('', _('None'))]
155 157
                 )
156 158
         return db_field.formfield(**kwargs)
@@ -787,49 +789,37 @@ def render_change_form(self, request, context, add=False, change=False, form_url
787 789
             "admin/change_form.html"
788 790
         ], context, current_app=self.admin_site.name)
789 791
 
790  
-    def response_add(self, request, obj, post_url_continue='../%s/',
791  
-                     continue_editing_url=None, add_another_url=None,
792  
-                     hasperm_url=None, noperm_url=None):
  792
+    def response_add(self, request, obj, post_url_continue=None):
793 793
         """
794 794
         Determines the HttpResponse for the add_view stage.
795  
-
796  
-        :param request: HttpRequest instance.
797  
-        :param obj: Object just added.
798  
-        :param post_url_continue: Deprecated/undocumented.
799  
-        :param continue_editing_url: URL where user will be redirected after
800  
-                                     pressing 'Save and continue editing'.
801  
-        :param add_another_url: URL where user will be redirected after
802  
-                                pressing 'Save and add another'.
803  
-        :param hasperm_url: URL to redirect after a successful object creation
804  
-                            when the user has change permissions.
805  
-        :param noperm_url: URL to redirect after a successful object creation
806  
-                           when the user has no change permissions.
807  
-        """
808  
-        if post_url_continue != '../%s/':
809  
-            warnings.warn("The undocumented 'post_url_continue' argument to "
810  
-                          "ModelAdmin.response_add() is deprecated, use the new "
811  
-                          "*_url arguments instead.", DeprecationWarning,
812  
-                          stacklevel=2)
  795
+        """
813 796
         opts = obj._meta
814  
-        pk_value = obj.pk
815  
-        app_label = opts.app_label
816  
-        model_name = opts.module_name
817  
-        site_name = self.admin_site.name
  797
+        pk_value = obj._get_pk_val()
818 798
 
819 799
         msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
820  
-
821 800
         # Here, we distinguish between different save types by checking for
822 801
         # the presence of keys in request.POST.
823 802
         if "_continue" in request.POST:
824 803
             msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
825 804
             self.message_user(request, msg)
826  
-            if continue_editing_url is None:
827  
-                continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name)
828  
-            url = reverse(continue_editing_url, args=(quote(pk_value),),
829  
-                          current_app=site_name)
  805
+            if post_url_continue is None:
  806
+                post_url_continue = reverse('admin:%s_%s_change' %
  807
+                                            (opts.app_label, opts.module_name),
  808
+                                            args=(pk_value,),
  809
+                                            current_app=self.admin_site.name)
  810
+            else:
  811
+                try:
  812
+                    post_url_continue = post_url_continue % pk_value
  813
+                    warnings.warn(
  814
+                        "The use of string formats for post_url_continue "
  815
+                        "in ModelAdmin.response_add() is deprecated. Provide "
  816
+                        "a pre-formatted url instead.",
  817
+                        DeprecationWarning, stacklevel=2)
  818
+                except TypeError:
  819
+                    pass
830 820
             if "_popup" in request.POST:
831  
-                url += "?_popup=1"
832  
-            return HttpResponseRedirect(url)
  821
+                post_url_continue += "?_popup=1"
  822
+            return HttpResponseRedirect(post_url_continue)
833 823
 
834 824
         if "_popup" in request.POST:
835 825
             return HttpResponse(
@@ -840,102 +830,61 @@ def response_add(self, request, obj, post_url_continue='../%s/',
840 830
         elif "_addanother" in request.POST:
841 831
             msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict
842 832
             self.message_user(request, msg)
843  
-            if add_another_url is None:
844  
-                add_another_url = 'admin:%s_%s_add' % (app_label, model_name)
845  
-            url = reverse(add_another_url, current_app=site_name)
846  
-            return HttpResponseRedirect(url)
  833
+            return HttpResponseRedirect(request.path)
847 834
         else:
848 835
             msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict
849 836
             self.message_user(request, msg)
  837
+            return self.response_post_save(request, obj)
850 838
 
851  
-            # Figure out where to redirect. If the user has change permission,
852  
-            # redirect to the change-list page for this object. Otherwise,
853  
-            # redirect to the admin index.
854  
-            if self.has_change_permission(request, None):
855  
-                if hasperm_url is None:
856  
-                    hasperm_url = 'admin:%s_%s_changelist' % (app_label, model_name)
857  
-                url = reverse(hasperm_url, current_app=site_name)
858  
-            else:
859  
-                if noperm_url is None:
860  
-                    noperm_url = 'admin:index'
861  
-                url = reverse(noperm_url, current_app=site_name)
862  
-            return HttpResponseRedirect(url)
863  
-
864  
-    def response_change(self, request, obj, continue_editing_url=None,
865  
-                        save_as_new_url=None, add_another_url=None,
866  
-                        hasperm_url=None, noperm_url=None):
  839
+    def response_change(self, request, obj):
867 840
         """
868 841
         Determines the HttpResponse for the change_view stage.
869  
-
870  
-        :param request: HttpRequest instance.
871  
-        :param obj: Object just modified.
872  
-        :param continue_editing_url: URL where user will be redirected after
873  
-                                     pressing 'Save and continue editing'.
874  
-        :param save_as_new_url: URL where user will be redirected after pressing
875  
-                                'Save as new' (when applicable).
876  
-        :param add_another_url: URL where user will be redirected after pressing
877  
-                                'Save and add another'.
878  
-        :param hasperm_url: URL to redirect after a successful object edition when
879  
-                            the user has change permissions.
880  
-        :param noperm_url: URL to redirect after a successful object edition when
881  
-                           the user has no change permissions.
882 842
         """
883  
-        opts = obj._meta
  843
+        opts = self.model._meta
884 844
 
885  
-        app_label = opts.app_label
886  
-        model_name = opts.module_name
887  
-        site_name = self.admin_site.name
888  
-        verbose_name = opts.verbose_name
889  
-        # Handle proxy models automatically created by .only() or .defer().
890  
-        # Refs #14529
891  
-        if obj._deferred:
892  
-            opts_ = opts.proxy_for_model._meta
893  
-            verbose_name = opts_.verbose_name
894  
-            model_name = opts_.module_name
895  
-
896  
-        msg_dict = {'name': force_text(verbose_name), 'obj': force_text(obj)}
  845
+        pk_value = obj._get_pk_val()
897 846
 
  847
+        msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)}
898 848
         if "_continue" in request.POST:
899 849
             msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict
900 850
             self.message_user(request, msg)
901  
-            if continue_editing_url is None:
902  
-                continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name)
903  
-            url = reverse(continue_editing_url, args=(quote(obj.pk),),
904  
-                          current_app=site_name)
905  
-            if "_popup" in request.POST:
906  
-                url += "?_popup=1"
907  
-            return HttpResponseRedirect(url)
  851
+            if "_popup" in request.REQUEST:
  852
+                return HttpResponseRedirect(request.path + "?_popup=1")
  853
+            else:
  854
+                return HttpResponseRedirect(request.path)
908 855
         elif "_saveasnew" in request.POST:
909 856
             msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict
910 857
             self.message_user(request, msg)
911  
-            if save_as_new_url is None:
912  
-                save_as_new_url = 'admin:%s_%s_change' % (app_label, model_name)
913  
-            url = reverse(save_as_new_url, args=(quote(obj.pk),),
914  
-                          current_app=site_name)
915  
-            return HttpResponseRedirect(url)
  858
+            return HttpResponseRedirect(reverse('admin:%s_%s_change' %
  859
+                                        (opts.app_label, opts.module_name),
  860
+                                        args=(pk_value,),
  861
+                                        current_app=self.admin_site.name))
916 862
         elif "_addanother" in request.POST:
917 863
             msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict
918 864
             self.message_user(request, msg)
919  
-            if add_another_url is None:
920  
-                add_another_url = 'admin:%s_%s_add' % (app_label, model_name)
921  
-            url = reverse(add_another_url, current_app=site_name)
922  
-            return HttpResponseRedirect(url)
  865
+            return HttpResponseRedirect(reverse('admin:%s_%s_add' %
  866
+                                        (opts.app_label, opts.module_name),
  867
+                                        current_app=self.admin_site.name))
923 868
         else:
924 869
             msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict
925 870
             self.message_user(request, msg)
926  
-            # Figure out where to redirect. If the user has change permission,
927  
-            # redirect to the change-list page for this object. Otherwise,
928  
-            # redirect to the admin index.
929  
-            if self.has_change_permission(request, None):
930  
-                if hasperm_url is None:
931  
-                    hasperm_url = 'admin:%s_%s_changelist' % (app_label,
932  
-                                                              model_name)
933  
-                url = reverse(hasperm_url, current_app=site_name)
934  
-            else:
935  
-                if noperm_url is None:
936  
-                    noperm_url = 'admin:index'
937  
-                url = reverse(noperm_url, current_app=site_name)
938  
-            return HttpResponseRedirect(url)
  871
+            return self.response_post_save(request, obj)
  872
+
  873
+    def response_post_save(self, request, obj):
  874
+        """
  875
+        Figure out where to redirect after the 'Save' button has been pressed.
  876
+        If the user has change permission, redirect to the change-list page for
  877
+        this object. Otherwise, redirect to the admin index.
  878
+        """
  879
+        opts = self.model._meta
  880
+        if self.has_change_permission(request, None):
  881
+            post_url = reverse('admin:%s_%s_changelist' %
  882
+                               (opts.app_label, opts.module_name),
  883
+                               current_app=self.admin_site.name)
  884
+        else:
  885
+            post_url = reverse('admin:index',
  886
+                               current_app=self.admin_site.name)
  887
+        return HttpResponseRedirect(post_url)
939 888
 
940 889
     def response_action(self, request, queryset):
941 890
         """
5  django/contrib/auth/admin.py
@@ -153,7 +153,7 @@ def user_change_password(self, request, id, form_url=''):
153 153
             'admin/auth/user/change_password.html',
154 154
             context, current_app=self.admin_site.name)
155 155
 
156  
-    def response_add(self, request, obj, **kwargs):
  156
+    def response_add(self, request, obj, post_url_continue=None):
157 157
         """
158 158
         Determines the HttpResponse for the add_view stage. It mostly defers to
159 159
         its superclass implementation but is customized because the User model
@@ -166,7 +166,8 @@ def response_add(self, request, obj, **kwargs):
166 166
         # * We are adding a user in a popup
167 167
         if '_addanother' not in request.POST and '_popup' not in request.POST:
168 168
             request.POST['_continue'] = 1
169  
-        return super(UserAdmin, self).response_add(request, obj, **kwargs)
  169
+        return super(UserAdmin, self).response_add(request, obj,
  170
+                                                   post_url_continue)
170 171
 
171 172
 admin.site.register(Group, GroupAdmin)
172 173
 admin.site.register(User, UserAdmin)
6  docs/internals/deprecation.txt
@@ -268,6 +268,12 @@ these changes.
268 268
 * ``django.contrib.markup`` will be removed following an accelerated
269 269
   deprecation.
270 270
 
  271
+* The value for the ``post_url_continue`` parameter in
  272
+  ``ModelAdmin.response_add()`` will have to be either ``None`` (to redirect
  273
+  to the newly created object's edit page) or a pre-formatted url. String
  274
+  formats, such as the previous default ``'../%s/'``, will not be accepted any
  275
+  more.
  276
+
271 277
 1.7
272 278
 ---
273 279
 
51  tests/regressiontests/admin_custom_urls/models.py
... ...
@@ -1,7 +1,9 @@
1 1
 from functools import update_wrapper
2 2
 
3 3
 from django.contrib import admin
  4
+from django.core.urlresolvers import reverse
4 5
 from django.db import models
  6
+from django.http import HttpResponseRedirect
5 7
 from django.utils.encoding import python_2_unicode_compatible
6 8
 
7 9
 
@@ -49,41 +51,38 @@ def wrapper(*args, **kwargs):
49 51
         ) + self.remove_url(view_name)
50 52
 
51 53
 
52  
-admin.site.register(Action, ActionAdmin)
  54
+class Person(models.Model):
  55
+    name = models.CharField(max_length=20)
53 56
 
  57
+class PersonAdmin(admin.ModelAdmin):
54 58
 
55  
-class Person(models.Model):
56  
-    nick = models.CharField(max_length=20)
  59
+    def response_post_save(self, request, obj):
  60
+        return HttpResponseRedirect(
  61
+            reverse('admin:admin_custom_urls_person_history', args=[obj.pk]))
57 62
 
58 63
 
59  
-class PersonAdmin(admin.ModelAdmin):
60  
-    """A custom ModelAdmin that customizes the deprecated post_url_continue
61  
-    argument to response_add()"""
62  
-    def response_add(self, request, obj, post_url_continue='../%s/continue/',
63  
-                     continue_url=None, add_url=None, hasperm_url=None,
64  
-                     noperm_url=None):
65  
-        return super(PersonAdmin, self).response_add(request, obj,
66  
-                                                     post_url_continue,
67  
-                                                     continue_url, add_url,
68  
-                                                     hasperm_url, noperm_url)
  64
+class Car(models.Model):
  65
+    name = models.CharField(max_length=20)
69 66
 
  67
+class CarAdmin(admin.ModelAdmin):
70 68
 
71  
-admin.site.register(Person, PersonAdmin)
  69
+    def response_add(self, request, obj, post_url_continue=None):
  70
+        return super(CarAdmin, self).response_add(
  71
+            request, obj, post_url_continue=reverse('admin:admin_custom_urls_car_history', args=[obj.pk]))
72 72
 
73 73
 
74  
-class City(models.Model):
  74
+class CarDeprecated(models.Model):
  75
+    """ This class must be removed in Django 1.6 """
75 76
     name = models.CharField(max_length=20)
76 77
 
77  
-
78  
-class CityAdmin(admin.ModelAdmin):
79  
-    """A custom ModelAdmin that redirects to the changelist when the user
80  
-    presses the 'Save and add another' button when adding a model instance."""
81  
-    def response_add(self, request, obj,
82  
-                     add_another_url='admin:admin_custom_urls_city_changelist',
83  
-                     **kwargs):
84  
-        return super(CityAdmin, self).response_add(request, obj,
85  
-                                                   add_another_url=add_another_url,
86  
-                                                   **kwargs)
  78
+class CarDeprecatedAdmin(admin.ModelAdmin):
  79
+    """ This class must be removed in Django 1.6 """
  80
+    def response_add(self, request, obj, post_url_continue=None):
  81
+        return super(CarDeprecatedAdmin, self).response_add(
  82
+            request, obj, post_url_continue='../%s/history/')
87 83
 
88 84
 
89  
-admin.site.register(City, CityAdmin)
  85
+admin.site.register(Action, ActionAdmin)
  86
+admin.site.register(Person, PersonAdmin)
  87
+admin.site.register(Car, CarAdmin)
  88
+admin.site.register(CarDeprecated, CarDeprecatedAdmin)
81  tests/regressiontests/admin_custom_urls/tests.py
... ...
@@ -1,5 +1,4 @@
1 1
 from __future__ import absolute_import, unicode_literals
2  
-
3 2
 import warnings
4 3
 
5 4
 from django.contrib.admin.util import quote
@@ -8,7 +7,7 @@
8 7
 from django.test import TestCase
9 8
 from django.test.utils import override_settings
10 9
 
11  
-from .models import Action, Person, City
  10
+from .models import Action, Person, Car, CarDeprecated
12 11
 
13 12
 
14 13
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
@@ -86,8 +85,8 @@ def testAdminUrlsNoClash(self):
86 85
 
87 86
 
88 87
 @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
89  
-class CustomUrlsWorkflowTests(TestCase):
90  
-    fixtures = ['users.json']
  88
+class CustomRedirects(TestCase):
  89
+    fixtures = ['users.json', 'actions.json']
91 90
 
92 91
     def setUp(self):
93 92
         self.client.login(username='super', password='secret')
@@ -95,33 +94,49 @@ def setUp(self):
95 94
     def tearDown(self):
96 95
         self.client.logout()
97 96
 
98  
-    def test_old_argument_deprecation(self):
99  
-        """Test reporting of post_url_continue deprecation."""
100  
-        post_data = {
101  
-            'nick': 'johndoe',
102  
-        }
103  
-        cnt = Person.objects.count()
104  
-        with warnings.catch_warnings(record=True) as w:
105  
-            warnings.simplefilter("always")
106  
-            response = self.client.post(reverse('admin:admin_custom_urls_person_add'), post_data)
107  
-            self.assertEqual(response.status_code, 302)
108  
-            self.assertEqual(Person.objects.count(), cnt + 1)
109  
-            # We should get a DeprecationWarning
110  
-            self.assertEqual(len(w), 1)
111  
-            self.assertTrue(isinstance(w[0].message, DeprecationWarning))
112  
-
113  
-    def test_custom_add_another_redirect(self):
114  
-        """Test customizability of post-object-creation redirect URL."""
115  
-        post_data = {
116  
-            'name': 'Rome',
117  
-            '_addanother': '1',
118  
-        }
119  
-        cnt = City.objects.count()
  97
+    def test_post_save_redirect(self):
  98
+        """
  99
+        Ensures that ModelAdmin.response_post_save() controls the redirection
  100
+        after the 'Save' button has been pressed.
  101
+        Refs 8001, 18310, 19505.
  102
+        """
  103
+        post_data = { 'name': 'John Doe', }
  104
+        self.assertEqual(Person.objects.count(), 0)
  105
+        response = self.client.post(
  106
+            reverse('admin:admin_custom_urls_person_add'), post_data)
  107
+        persons = Person.objects.all()
  108
+        self.assertEqual(len(persons), 1)
  109
+        self.assertRedirects(
  110
+            response, reverse('admin:admin_custom_urls_person_history', args=[persons[0].pk]))
  111
+
  112
+    def test_post_url_continue(self):
  113
+        """
  114
+        Ensures that the ModelAdmin.response_add()'s parameter `post_url_continue`
  115
+        controls the redirection after an object has been created.
  116
+        """
  117
+        post_data = { 'name': 'SuperFast', '_continue': '1' }
  118
+        self.assertEqual(Car.objects.count(), 0)
  119
+        response = self.client.post(
  120
+            reverse('admin:admin_custom_urls_car_add'), post_data)
  121
+        cars = Car.objects.all()
  122
+        self.assertEqual(len(cars), 1)
  123
+        self.assertRedirects(
  124
+            response, reverse('admin:admin_custom_urls_car_history', args=[cars[0].pk]))
  125
+
  126
+    def test_post_url_continue_string_formats(self):
  127
+        """
  128
+        Ensures that string formats are accepted for post_url_continue. This
  129
+        is a deprecated functionality that will be removed in Django 1.6 along
  130
+        with this test.
  131
+        """
120 132
         with warnings.catch_warnings(record=True) as w:
121  
-            # POST to the view whose post-object-creation redir URL argument we
122  
-            # are customizing (object creation)
123  
-            response = self.client.post(reverse('admin:admin_custom_urls_city_add'), post_data)
124  
-            self.assertEqual(City.objects.count(), cnt + 1)
125  
-            # Check that it redirected to the URL we set
126  
-            self.assertRedirects(response, reverse('admin:admin_custom_urls_city_changelist'))
127  
-            self.assertEqual(len(w), 0) # We should get no DeprecationWarning
  133
+            post_data = { 'name': 'SuperFast', '_continue': '1' }
  134
+            self.assertEqual(Car.objects.count(), 0)
  135
+            response = self.client.post(
  136
+                reverse('admin:admin_custom_urls_cardeprecated_add'), post_data)
  137
+            cars = CarDeprecated.objects.all()
  138
+            self.assertEqual(len(cars), 1)
  139
+            self.assertRedirects(
  140
+                response, reverse('admin:admin_custom_urls_cardeprecated_history', args=[cars[0].pk]))
  141
+        self.assertEqual(len(w), 1)
  142
+        self.assertTrue(isinstance(w[0].message, DeprecationWarning))

0 notes on commit 35d1cd0

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