Loading django/contrib/admin/options.py +51 −15 Original line number Diff line number Diff line from __future__ import unicode_literals import copy import operator from collections import OrderedDict Loading Loading @@ -38,8 +40,8 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils import six from django.utils.decorators import method_decorator from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.html import escape, escapejs from django.utils.http import urlencode from django.utils.html import escape, escapejs, format_html from django.utils.http import urlencode, urlquote from django.utils.safestring import mark_safe from django.utils.text import capfirst, get_text_list from django.utils.translation import string_concat, ugettext as _, ungettext Loading Loading @@ -1058,7 +1060,20 @@ class ModelAdmin(BaseModelAdmin): opts = obj._meta pk_value = obj._get_pk_val() preserved_filters = self.get_preserved_filters(request) msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} obj_url = reverse( 'admin:%s_%s_change' % (opts.app_label, opts.model_name), args=(quote(pk_value),), current_app=self.admin_site.name, ) # Add a link to the object's change form if the user can edit the obj. if self.has_change_permission(request, obj): obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj) else: obj_repr = force_text(obj) msg_dict = { 'name': force_text(opts.verbose_name), 'obj': obj_repr, } # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. Loading @@ -1075,13 +1090,13 @@ class ModelAdmin(BaseModelAdmin): }) elif "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully. You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) if post_url_continue is None: post_url_continue = reverse('admin:%s_%s_change' % (opts.app_label, opts.model_name), args=(quote(pk_value),), current_app=self.admin_site.name) post_url_continue = obj_url post_url_continue = add_preserved_filters( {'preserved_filters': preserved_filters, 'opts': opts}, post_url_continue Loading @@ -1089,14 +1104,20 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(post_url_continue) elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully. You may add another {name} below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = request.path redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url) return HttpResponseRedirect(redirect_url) else: msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) return self.response_post_save_add(request, obj) Loading @@ -1122,16 +1143,25 @@ class ModelAdmin(BaseModelAdmin): pk_value = obj._get_pk_val() preserved_filters = self.get_preserved_filters(request) msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} msg_dict = { 'name': force_text(opts.verbose_name), 'obj': format_html('<a href="{}">{}</a>', urlquote(request.path), obj), } if "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict msg = format_html( _('The {name} "{obj}" was changed successfully. You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = request.path redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url) return HttpResponseRedirect(redirect_url) elif "_saveasnew" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully. You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = reverse('admin:%s_%s_change' % (opts.app_label, opts.model_name), Loading @@ -1141,7 +1171,10 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(redirect_url) elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict msg = format_html( _('The {name} "{obj}" was changed successfully. You may add another {name} below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = reverse('admin:%s_%s_add' % (opts.app_label, opts.model_name), Loading @@ -1150,7 +1183,10 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(redirect_url) else: msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict msg = format_html( _('The {name} "{obj}" was changed successfully.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) return self.response_post_save_change(request, obj) Loading docs/releases/1.10.txt +3 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,9 @@ Minor features link <django.contrib.admin.AdminSite.site_url>` at the top of each admin page will now point to ``request.META['SCRIPT_NAME']`` if set, instead of ``/``. * The success message that appears after adding or editing an object now contains a link to the object's change form. :mod:`django.contrib.admindocs` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Loading tests/admin_views/tests.py +51 −19 Original line number Diff line number Diff line Loading @@ -2025,6 +2025,28 @@ class AdminViewPermissionsTest(TestCase): self.assertNotContains(response, 'admin_views') self.assertNotContains(response, 'Articles') def test_post_save_message_no_forbidden_links_visible(self): """ Post-save message shouldn't contain a link to the change form if the user doen't have the change permission. """ login = self.client.post(reverse('admin:login'), self.adduser_login) self.assertRedirects(login, self.index_url) # Emulate Article creation for user with add-only permission. post_data = { "title": "Fun & games", "content": "Some content", "date_0": "2015-10-31", "date_1": "16:35:00", "_save": "Save", } response = self.client.post(reverse('admin:admin_views_article_add'), post_data, follow=True) self.assertContains( response, '<li class="success">The article "Fun & games" was added successfully.</li>', html=True ) @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], ROOT_URLCONF="admin_views.urls") Loading Loading @@ -3801,10 +3823,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(CoverLetter.objects.count(), 1) # Message should contain non-ugly model verbose name pk = CoverLetter.objects.all()[0].pk self.assertContains( response, '<li class="success">The cover letter "Candidate, Best" was added successfully.</li>', html=True '<li class="success">The cover letter "<a href="%s">' 'Candidate, Best</a>" was added successfully.</li>' % reverse('admin:admin_views_coverletter_change', args=(pk,)), html=True ) # model has no __unicode__ method Loading @@ -3819,10 +3843,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(ShortMessage.objects.count(), 1) # Message should contain non-ugly model verbose name pk = ShortMessage.objects.all()[0].pk self.assertContains( response, '<li class="success">The short message "ShortMessage object" was added successfully.</li>', html=True '<li class="success">The short message "<a href="%s">' 'ShortMessage object</a>" was added successfully.</li>' % reverse('admin:admin_views_shortmessage_change', args=(pk,)), html=True ) def test_add_model_modeladmin_only_qs(self): Loading @@ -3840,10 +3866,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(Telegram.objects.count(), 1) # Message should contain non-ugly model verbose name pk = Telegram.objects.all()[0].pk self.assertContains( response, '<li class="success">The telegram "Urgent telegram" was added successfully.</li>', html=True '<li class="success">The telegram "<a href="%s">' 'Urgent telegram</a>" was added successfully.</li>' % reverse('admin:admin_views_telegram_change', args=(pk,)), html=True ) # model has no __unicode__ method Loading @@ -3858,10 +3886,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(Paper.objects.count(), 1) # Message should contain non-ugly model verbose name pk = Paper.objects.all()[0].pk self.assertContains( response, '<li class="success">The paper "Paper object" was added successfully.</li>', html=True '<li class="success">The paper "<a href="%s">' 'Paper object</a>" was added successfully.</li>' % reverse('admin:admin_views_paper_change', args=(pk,)), html=True ) def test_edit_model_modeladmin_defer_qs(self): Loading @@ -3885,8 +3915,9 @@ class AdminCustomQuerysetTest(TestCase): # representation is set by model's __unicode__() self.assertContains( response, '<li class="success">The cover letter "John Doe II" was changed successfully.</li>', html=True '<li class="success">The cover letter "<a href="%s">' 'John Doe II</a>" was changed successfully.</li>' % reverse('admin:admin_views_coverletter_change', args=(cl.pk,)), html=True ) # model has no __unicode__ method Loading @@ -3906,11 +3937,10 @@ class AdminCustomQuerysetTest(TestCase): # Message should contain non-ugly model verbose name. The ugly(!) # instance representation is set by six.text_type() self.assertContains( response, ( '<li class="success">The short message ' '"ShortMessage_Deferred_timestamp object" was ' 'changed successfully.</li>' ), html=True response, '<li class="success">The short message "<a href="%s">' 'ShortMessage_Deferred_timestamp object</a>" was changed successfully.</li>' % reverse('admin:admin_views_shortmessage_change', args=(sm.pk,)), html=True ) def test_edit_model_modeladmin_only_qs(self): Loading @@ -3934,8 +3964,9 @@ class AdminCustomQuerysetTest(TestCase): # representation is set by model's __unicode__() self.assertContains( response, '<li class="success">The telegram "Telegram without typo" was changed successfully.</li>', html=True '<li class="success">The telegram "<a href="%s">' 'Telegram without typo</a>" was changed successfully.</li>' % reverse('admin:admin_views_telegram_change', args=(t.pk,)), html=True ) # model has no __unicode__ method Loading @@ -3956,8 +3987,9 @@ class AdminCustomQuerysetTest(TestCase): # instance representation is set by six.text_type() self.assertContains( response, '<li class="success">The paper "Paper_Deferred_author object" was changed successfully.</li>', html=True '<li class="success">The paper "<a href="%s">' 'Paper_Deferred_author object</a>" was changed successfully.</li>' % reverse('admin:admin_views_paper_change', args=(p.pk,)), html=True ) def test_history_view_custom_qs(self): Loading Loading
django/contrib/admin/options.py +51 −15 Original line number Diff line number Diff line from __future__ import unicode_literals import copy import operator from collections import OrderedDict Loading Loading @@ -38,8 +40,8 @@ from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils import six from django.utils.decorators import method_decorator from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.html import escape, escapejs from django.utils.http import urlencode from django.utils.html import escape, escapejs, format_html from django.utils.http import urlencode, urlquote from django.utils.safestring import mark_safe from django.utils.text import capfirst, get_text_list from django.utils.translation import string_concat, ugettext as _, ungettext Loading Loading @@ -1058,7 +1060,20 @@ class ModelAdmin(BaseModelAdmin): opts = obj._meta pk_value = obj._get_pk_val() preserved_filters = self.get_preserved_filters(request) msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} obj_url = reverse( 'admin:%s_%s_change' % (opts.app_label, opts.model_name), args=(quote(pk_value),), current_app=self.admin_site.name, ) # Add a link to the object's change form if the user can edit the obj. if self.has_change_permission(request, obj): obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj) else: obj_repr = force_text(obj) msg_dict = { 'name': force_text(opts.verbose_name), 'obj': obj_repr, } # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. Loading @@ -1075,13 +1090,13 @@ class ModelAdmin(BaseModelAdmin): }) elif "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully. You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) if post_url_continue is None: post_url_continue = reverse('admin:%s_%s_change' % (opts.app_label, opts.model_name), args=(quote(pk_value),), current_app=self.admin_site.name) post_url_continue = obj_url post_url_continue = add_preserved_filters( {'preserved_filters': preserved_filters, 'opts': opts}, post_url_continue Loading @@ -1089,14 +1104,20 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(post_url_continue) elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully. You may add another {name} below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = request.path redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url) return HttpResponseRedirect(redirect_url) else: msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) return self.response_post_save_add(request, obj) Loading @@ -1122,16 +1143,25 @@ class ModelAdmin(BaseModelAdmin): pk_value = obj._get_pk_val() preserved_filters = self.get_preserved_filters(request) msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} msg_dict = { 'name': force_text(opts.verbose_name), 'obj': format_html('<a href="{}">{}</a>', urlquote(request.path), obj), } if "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict msg = format_html( _('The {name} "{obj}" was changed successfully. You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = request.path redirect_url = add_preserved_filters({'preserved_filters': preserved_filters, 'opts': opts}, redirect_url) return HttpResponseRedirect(redirect_url) elif "_saveasnew" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict msg = format_html( _('The {name} "{obj}" was added successfully. You may edit it again below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = reverse('admin:%s_%s_change' % (opts.app_label, opts.model_name), Loading @@ -1141,7 +1171,10 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(redirect_url) elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict msg = format_html( _('The {name} "{obj}" was changed successfully. You may add another {name} below.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) redirect_url = reverse('admin:%s_%s_add' % (opts.app_label, opts.model_name), Loading @@ -1150,7 +1183,10 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(redirect_url) else: msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict msg = format_html( _('The {name} "{obj}" was changed successfully.'), **msg_dict ) self.message_user(request, msg, messages.SUCCESS) return self.response_post_save_change(request, obj) Loading
docs/releases/1.10.txt +3 −0 Original line number Diff line number Diff line Loading @@ -36,6 +36,9 @@ Minor features link <django.contrib.admin.AdminSite.site_url>` at the top of each admin page will now point to ``request.META['SCRIPT_NAME']`` if set, instead of ``/``. * The success message that appears after adding or editing an object now contains a link to the object's change form. :mod:`django.contrib.admindocs` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Loading
tests/admin_views/tests.py +51 −19 Original line number Diff line number Diff line Loading @@ -2025,6 +2025,28 @@ class AdminViewPermissionsTest(TestCase): self.assertNotContains(response, 'admin_views') self.assertNotContains(response, 'Articles') def test_post_save_message_no_forbidden_links_visible(self): """ Post-save message shouldn't contain a link to the change form if the user doen't have the change permission. """ login = self.client.post(reverse('admin:login'), self.adduser_login) self.assertRedirects(login, self.index_url) # Emulate Article creation for user with add-only permission. post_data = { "title": "Fun & games", "content": "Some content", "date_0": "2015-10-31", "date_1": "16:35:00", "_save": "Save", } response = self.client.post(reverse('admin:admin_views_article_add'), post_data, follow=True) self.assertContains( response, '<li class="success">The article "Fun & games" was added successfully.</li>', html=True ) @override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'], ROOT_URLCONF="admin_views.urls") Loading Loading @@ -3801,10 +3823,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(CoverLetter.objects.count(), 1) # Message should contain non-ugly model verbose name pk = CoverLetter.objects.all()[0].pk self.assertContains( response, '<li class="success">The cover letter "Candidate, Best" was added successfully.</li>', html=True '<li class="success">The cover letter "<a href="%s">' 'Candidate, Best</a>" was added successfully.</li>' % reverse('admin:admin_views_coverletter_change', args=(pk,)), html=True ) # model has no __unicode__ method Loading @@ -3819,10 +3843,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(ShortMessage.objects.count(), 1) # Message should contain non-ugly model verbose name pk = ShortMessage.objects.all()[0].pk self.assertContains( response, '<li class="success">The short message "ShortMessage object" was added successfully.</li>', html=True '<li class="success">The short message "<a href="%s">' 'ShortMessage object</a>" was added successfully.</li>' % reverse('admin:admin_views_shortmessage_change', args=(pk,)), html=True ) def test_add_model_modeladmin_only_qs(self): Loading @@ -3840,10 +3866,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(Telegram.objects.count(), 1) # Message should contain non-ugly model verbose name pk = Telegram.objects.all()[0].pk self.assertContains( response, '<li class="success">The telegram "Urgent telegram" was added successfully.</li>', html=True '<li class="success">The telegram "<a href="%s">' 'Urgent telegram</a>" was added successfully.</li>' % reverse('admin:admin_views_telegram_change', args=(pk,)), html=True ) # model has no __unicode__ method Loading @@ -3858,10 +3886,12 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(Paper.objects.count(), 1) # Message should contain non-ugly model verbose name pk = Paper.objects.all()[0].pk self.assertContains( response, '<li class="success">The paper "Paper object" was added successfully.</li>', html=True '<li class="success">The paper "<a href="%s">' 'Paper object</a>" was added successfully.</li>' % reverse('admin:admin_views_paper_change', args=(pk,)), html=True ) def test_edit_model_modeladmin_defer_qs(self): Loading @@ -3885,8 +3915,9 @@ class AdminCustomQuerysetTest(TestCase): # representation is set by model's __unicode__() self.assertContains( response, '<li class="success">The cover letter "John Doe II" was changed successfully.</li>', html=True '<li class="success">The cover letter "<a href="%s">' 'John Doe II</a>" was changed successfully.</li>' % reverse('admin:admin_views_coverletter_change', args=(cl.pk,)), html=True ) # model has no __unicode__ method Loading @@ -3906,11 +3937,10 @@ class AdminCustomQuerysetTest(TestCase): # Message should contain non-ugly model verbose name. The ugly(!) # instance representation is set by six.text_type() self.assertContains( response, ( '<li class="success">The short message ' '"ShortMessage_Deferred_timestamp object" was ' 'changed successfully.</li>' ), html=True response, '<li class="success">The short message "<a href="%s">' 'ShortMessage_Deferred_timestamp object</a>" was changed successfully.</li>' % reverse('admin:admin_views_shortmessage_change', args=(sm.pk,)), html=True ) def test_edit_model_modeladmin_only_qs(self): Loading @@ -3934,8 +3964,9 @@ class AdminCustomQuerysetTest(TestCase): # representation is set by model's __unicode__() self.assertContains( response, '<li class="success">The telegram "Telegram without typo" was changed successfully.</li>', html=True '<li class="success">The telegram "<a href="%s">' 'Telegram without typo</a>" was changed successfully.</li>' % reverse('admin:admin_views_telegram_change', args=(t.pk,)), html=True ) # model has no __unicode__ method Loading @@ -3956,8 +3987,9 @@ class AdminCustomQuerysetTest(TestCase): # instance representation is set by six.text_type() self.assertContains( response, '<li class="success">The paper "Paper_Deferred_author object" was changed successfully.</li>', html=True '<li class="success">The paper "<a href="%s">' 'Paper_Deferred_author object</a>" was changed successfully.</li>' % reverse('admin:admin_views_paper_change', args=(p.pk,)), html=True ) def test_history_view_custom_qs(self): Loading