Fixed #18658 -- Improved ModelAdmin.message_user API

Thanks to Lowe Thiderman for the patch and tests
This commit is contained in:
Preston Holmes 2012-11-17 22:53:31 +01:00
parent 8b659e439b
commit edf7ad36fa
8 changed files with 133 additions and 7 deletions

View File

@ -531,6 +531,7 @@ answer newbie questions, and generally made Django that much better:
Terry Huang <terryh.tp@gmail.com> Terry Huang <terryh.tp@gmail.com>
Travis Terry <tdterry7@gmail.com> Travis Terry <tdterry7@gmail.com>
thebjorn <bp@datakortet.no> thebjorn <bp@datakortet.no>
Lowe Thiderman <lowe.thiderman@gmail.com>
Zach Thompson <zthompson47@gmail.com> Zach Thompson <zthompson47@gmail.com>
Michael Thornhill <michael.thornhill@gmail.com> Michael Thornhill <michael.thornhill@gmail.com>
Deepak Thukral <deep.thukral@gmail.com> Deepak Thukral <deep.thukral@gmail.com>

View File

@ -691,12 +691,30 @@ class ModelAdmin(BaseModelAdmin):
change_message = ' '.join(change_message) change_message = ' '.join(change_message)
return change_message or _('No fields changed.') return change_message or _('No fields changed.')
def message_user(self, request, message): def message_user(self, request, message, level=messages.INFO, extra_tags='',
fail_silently=False):
""" """
Send a message to the user. The default implementation Send a message to the user. The default implementation
posts a message using the django.contrib.messages backend. posts a message using the django.contrib.messages backend.
Exposes almost the same API as messages.add_message(), but accepts the
positional arguments in a different order to maintain backwards
compatibility. For convenience, it accepts the `level` argument as
a string rather than the ususal level number.
""" """
messages.info(request, message)
if not isinstance(level, int):
# attempt to get the level if passed a string
try:
level = getattr(messages.constants, level.upper())
except AttributeError:
levels = messages.constants.DEFAULT_TAGS.values()
levels_repr = ', '.join('`%s`' % l for l in levels)
raise ValueError('Bad message level string: `%s`. '
'Possible values are: %s' % (level, levels_repr))
messages.add_message(request, level, message, extra_tags=extra_tags,
fail_silently=fail_silently)
def save_form(self, request, form, change): def save_form(self, request, form, change):
""" """

View File

@ -140,6 +140,15 @@ That's really all there is to it! If you're itching to write your own actions,
you now know enough to get started. The rest of this document just covers more you now know enough to get started. The rest of this document just covers more
advanced techniques. advanced techniques.
Handling errors in actions
--------------------------
If there are foreseeable error conditions that may occur while running your
action, you should gracefully inform the user of the problem. This means
handling exceptions and and using
:meth:`django.contrib.admin.ModelAdmin.message_user` to display a user friendly
description of the problem in the response.
Advanced action techniques Advanced action techniques
========================== ==========================

View File

@ -1303,11 +1303,19 @@ templates used by the :class:`ModelAdmin` views:
return qs return qs
return qs.filter(author=request.user) return qs.filter(author=request.user)
.. method:: ModelAdmin.message_user(request, message) .. method:: ModelAdmin.message_user(request, message, level=messages.INFO, extra_tags='', fail_silently=False)
Sends a message to the user. The default implementation creates a message Sends a message to the user using the :mod:`django.contrib.messages`
using the :mod:`django.contrib.messages` backend. See the backend. See the :ref:`custom ModelAdmin example <custom-admin-action>`.
:ref:`custom ModelAdmin example <custom-admin-action>`.
.. versionadded:: 1.5
Keyword arguments allow you to change the message level, add extra CSS
tags, or fail silently if the ``contrib.messages`` framework is not
installed. These keyword arguments match those for
:func:`django.contrib.messages.add_message`, see that function's
documentation for more details. One difference is that the level may be
passed as a string label in addition to integer/constant.
.. method:: ModelAdmin.get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True) .. method:: ModelAdmin.get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True)

View File

@ -313,6 +313,11 @@ Django 1.5 also includes several smaller improvements worth noting:
DeprecationWarnings should be printed to the console in development DeprecationWarnings should be printed to the console in development
environments the way they have been in Python versions < 2.7. environments the way they have been in Python versions < 2.7.
* The API for :meth:`django.contrib.admin.ModelAdmin.message_user` method has
been modified to accept additional arguments adding capabilities similar to
:func:`django.contrib.messages.add_message`. This is useful for generating
error messages from admin actions.
Backwards incompatible changes in 1.5 Backwards incompatible changes in 1.5
===================================== =====================================

View File

@ -27,7 +27,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture,
Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug,
AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod,
AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated,
RelatedPrepopulated, UndeletableObject, Simple) RelatedPrepopulated, UndeletableObject, UserMessenger, Simple)
def callable_year(dt_value): def callable_year(dt_value):
@ -592,6 +592,28 @@ def callable_on_unknown(obj):
class AttributeErrorRaisingAdmin(admin.ModelAdmin): class AttributeErrorRaisingAdmin(admin.ModelAdmin):
list_display = [callable_on_unknown, ] list_display = [callable_on_unknown, ]
class MessageTestingAdmin(admin.ModelAdmin):
actions = ["message_debug", "message_info", "message_success",
"message_warning", "message_error", "message_extra_tags"]
def message_debug(self, request, selected):
self.message_user(request, "Test debug", level="debug")
def message_info(self, request, selected):
self.message_user(request, "Test info", level="info")
def message_success(self, request, selected):
self.message_user(request, "Test success", level="success")
def message_warning(self, request, selected):
self.message_user(request, "Test warning", level="warning")
def message_error(self, request, selected):
self.message_user(request, "Test error", level="error")
def message_extra_tags(self, request, selected):
self.message_user(request, "Test tags", extra_tags="extra_tag")
site = admin.AdminSite(name="admin") site = admin.AdminSite(name="admin")
site.register(Article, ArticleAdmin) site.register(Article, ArticleAdmin)
@ -667,6 +689,7 @@ site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin)
site.register(AdminOrderedCallable, AdminOrderedCallableAdmin) site.register(AdminOrderedCallable, AdminOrderedCallableAdmin)
site.register(Color2, CustomTemplateFilterColorAdmin) site.register(Color2, CustomTemplateFilterColorAdmin)
site.register(Simple, AttributeErrorRaisingAdmin) site.register(Simple, AttributeErrorRaisingAdmin)
site.register(UserMessenger, MessageTestingAdmin)
# Register core models we need in our tests # Register core models we need in our tests
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group

View File

@ -651,6 +651,10 @@ class UndeletableObject(models.Model):
""" """
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
class UserMessenger(models.Model):
"""
Dummy class for testing message_user functions on ModelAdmin
"""
class Simple(models.Model): class Simple(models.Model):
""" """

View File

@ -3697,3 +3697,61 @@ class AdminViewLogoutTest(TestCase):
self.assertEqual(response.template_name, 'admin/login.html') self.assertEqual(response.template_name, 'admin/login.html')
self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/') self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/')
self.assertContains(response, '<input type="hidden" name="next" value="/test_admin/admin/" />') self.assertContains(response, '<input type="hidden" name="next" value="/test_admin/admin/" />')
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',))
class AdminUserMessageTest(TestCase):
urls = "regressiontests.admin_views.urls"
fixtures = ['admin-views-users.xml']
def setUp(self):
self.client.login(username='super', password='secret')
def tearDown(self):
self.client.logout()
def send_message(self, level):
"""
Helper that sends a post to the dummy test methods and asserts that a
message with the level has appeared in the response.
"""
action_data = {
ACTION_CHECKBOX_NAME: [1],
'action': 'message_%s' % level,
'index': 0,
}
response = self.client.post('/test_admin/admin/admin_views/usermessenger/',
action_data, follow=True)
self.assertContains(response,
'<li class="%s">Test %s</li>' % (level, level),
html=True)
@override_settings(MESSAGE_LEVEL=10) # Set to DEBUG for this request
def test_message_debug(self):
self.send_message('debug')
def test_message_info(self):
self.send_message('info')
def test_message_success(self):
self.send_message('success')
def test_message_warning(self):
self.send_message('warning')
def test_message_error(self):
self.send_message('error')
def test_message_extra_tags(self):
action_data = {
ACTION_CHECKBOX_NAME: [1],
'action': 'message_extra_tags',
'index': 0,
}
response = self.client.post('/test_admin/admin/admin_views/usermessenger/',
action_data, follow=True)
self.assertContains(response,
'<li class="extra_tag info">Test tags</li>',
html=True)