Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

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 jphalip authored ramiro committed
167 django/contrib/admin/options.py
View
@@ -9,7 +9,7 @@
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 @@
# 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 @@ class IncorrectLookupParameters(Exception):
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 @@ def formfield_for_choice_field(self, db_field, request=None, **kwargs):
})
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 @@ def render_change_form(self, request, context, add=False, change=False, form_url
"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 @@ def response_add(self, request, obj, post_url_continue='../%s/',
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):
"""
5 django/contrib/auth/admin.py
View
@@ -153,7 +153,7 @@ def user_change_password(self, request, id, form_url=''):
'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 @@ def response_add(self, request, obj, **kwargs):
# * 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)
6 docs/internals/deprecation.txt
View
@@ -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
---
51 tests/regressiontests/admin_custom_urls/models.py
View
@@ -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 @@ def wrapper(*args, **kwargs):
) + self.remove_url(view_name)
-admin.site.register(Action, ActionAdmin)
+class Person(models.Model):
+ name = models.CharField(max_length=20)
+class PersonAdmin(admin.ModelAdmin):
-class Person(models.Model):
- nick = models.CharField(max_length=20)
+ def response_post_save(self, request, obj):
+ return HttpResponseRedirect(
+ reverse('admin:admin_custom_urls_person_history', args=[obj.pk]))
-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)
+class Car(models.Model):
+ name = models.CharField(max_length=20)
+class CarAdmin(admin.ModelAdmin):
-admin.site.register(Person, PersonAdmin)
+ 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 City(models.Model):
+class CarDeprecated(models.Model):
+ """ This class must be removed in Django 1.6 """
name = models.CharField(max_length=20)
-
-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)
+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(City, CityAdmin)
+admin.site.register(Action, ActionAdmin)
+admin.site.register(Person, PersonAdmin)
+admin.site.register(Car, CarAdmin)
+admin.site.register(CarDeprecated, CarDeprecatedAdmin)
81 tests/regressiontests/admin_custom_urls/tests.py
View
@@ -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.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 @@ def testAdminUrlsNoClash(self):
@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 @@ def setUp(self):
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_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_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_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))
Please sign in to comment.
Something went wrong with that request. Please try again.