mirror of
				https://github.com/django/django.git
				synced 2025-10-28 16:16:12 +00:00 
			
		
		
		
	Errors detected by Topy (https://github.com/intgr/topy), all changes
verified by hand.
Backport of 11d453bcad from master
		
	
		
			
				
	
	
		
			359 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			359 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| =============
 | |
| Admin actions
 | |
| =============
 | |
| 
 | |
| .. currentmodule:: django.contrib.admin
 | |
| 
 | |
| The basic workflow of Django's admin is, in a nutshell, "select an object,
 | |
| then change it." This works well for a majority of use cases. However, if you
 | |
| need to make the same change to many objects at once, this workflow can be
 | |
| quite tedious.
 | |
| 
 | |
| In these cases, Django's admin lets you write and register "actions" -- simple
 | |
| functions that get called with a list of objects selected on the change list
 | |
| page.
 | |
| 
 | |
| If you look at any change list in the admin, you'll see this feature in
 | |
| action; Django ships with a "delete selected objects" action available to all
 | |
| models. For example, here's the user module from Django's built-in
 | |
| :mod:`django.contrib.auth` app:
 | |
| 
 | |
| .. image:: _images/user_actions.png
 | |
| 
 | |
| .. warning::
 | |
| 
 | |
|     The "delete selected objects" action uses :meth:`QuerySet.delete()
 | |
|     <django.db.models.query.QuerySet.delete>` for efficiency reasons, which
 | |
|     has an important caveat: your model's ``delete()`` method will not be
 | |
|     called.
 | |
| 
 | |
|     If you wish to override this behavior, simply write a custom action which
 | |
|     accomplishes deletion in your preferred manner -- for example, by calling
 | |
|     ``Model.delete()`` for each of the selected items.
 | |
| 
 | |
|     For more background on bulk deletion, see the documentation on :ref:`object
 | |
|     deletion <topics-db-queries-delete>`.
 | |
| 
 | |
| Read on to find out how to add your own actions to this list.
 | |
| 
 | |
| Writing actions
 | |
| ===============
 | |
| 
 | |
| The easiest way to explain actions is by example, so let's dive in.
 | |
| 
 | |
| A common use case for admin actions is the bulk updating of a model. Imagine a
 | |
| simple news application with an ``Article`` model::
 | |
| 
 | |
|     from django.db import models
 | |
| 
 | |
|     STATUS_CHOICES = (
 | |
|         ('d', 'Draft'),
 | |
|         ('p', 'Published'),
 | |
|         ('w', 'Withdrawn'),
 | |
|     )
 | |
| 
 | |
|     class Article(models.Model):
 | |
|         title = models.CharField(max_length=100)
 | |
|         body = models.TextField()
 | |
|         status = models.CharField(max_length=1, choices=STATUS_CHOICES)
 | |
| 
 | |
|         def __str__(self):              # __unicode__ on Python 2
 | |
|             return self.title
 | |
| 
 | |
| A common task we might perform with a model like this is to update an
 | |
| article's status from "draft" to "published". We could easily do this in the
 | |
| admin one article at a time, but if we wanted to bulk-publish a group of
 | |
| articles, it'd be tedious. So, let's write an action that lets us change an
 | |
| article's status to "published."
 | |
| 
 | |
| Writing action functions
 | |
| ------------------------
 | |
| 
 | |
| First, we'll need to write a function that gets called when the action is
 | |
| trigged from the admin. Action functions are just regular functions that take
 | |
| three arguments:
 | |
| 
 | |
| * The current :class:`ModelAdmin`
 | |
| * An :class:`~django.http.HttpRequest` representing the current request,
 | |
| * A :class:`~django.db.models.query.QuerySet` containing the set of
 | |
|   objects selected by the user.
 | |
| 
 | |
| Our publish-these-articles function won't need the :class:`ModelAdmin` or the
 | |
| request object, but we will use the queryset::
 | |
| 
 | |
|     def make_published(modeladmin, request, queryset):
 | |
|         queryset.update(status='p')
 | |
| 
 | |
| .. note::
 | |
| 
 | |
|     For the best performance, we're using the queryset's :ref:`update method
 | |
|     <topics-db-queries-update>`. Other types of actions might need to deal
 | |
|     with each object individually; in these cases we'd just iterate over the
 | |
|     queryset::
 | |
| 
 | |
|         for obj in queryset:
 | |
|             do_something_with(obj)
 | |
| 
 | |
| That's actually all there is to writing an action! However, we'll take one
 | |
| more optional-but-useful step and give the action a "nice" title in the admin.
 | |
| By default, this action would appear in the action list as "Make published" --
 | |
| the function name, with underscores replaced by spaces. That's fine, but we
 | |
| can provide a better, more human-friendly name by giving the
 | |
| ``make_published`` function a ``short_description`` attribute::
 | |
| 
 | |
|     def make_published(modeladmin, request, queryset):
 | |
|         queryset.update(status='p')
 | |
|     make_published.short_description = "Mark selected stories as published"
 | |
| 
 | |
| .. note::
 | |
| 
 | |
|     This might look familiar; the admin's ``list_display`` option uses the
 | |
|     same technique to provide human-readable descriptions for callback
 | |
|     functions registered there, too.
 | |
| 
 | |
| Adding actions to the :class:`ModelAdmin`
 | |
| -----------------------------------------
 | |
| 
 | |
| Next, we'll need to inform our :class:`ModelAdmin` of the action. This works
 | |
| just like any other configuration option. So, the complete ``admin.py`` with
 | |
| the action and its registration would look like::
 | |
| 
 | |
|     from django.contrib import admin
 | |
|     from myapp.models import Article
 | |
| 
 | |
|     def make_published(modeladmin, request, queryset):
 | |
|         queryset.update(status='p')
 | |
|     make_published.short_description = "Mark selected stories as published"
 | |
| 
 | |
|     class ArticleAdmin(admin.ModelAdmin):
 | |
|         list_display = ['title', 'status']
 | |
|         ordering = ['title']
 | |
|         actions = [make_published]
 | |
| 
 | |
|     admin.site.register(Article, ArticleAdmin)
 | |
| 
 | |
| That code will give us an admin change list that looks something like this:
 | |
| 
 | |
| .. image:: _images/article_actions.png
 | |
| 
 | |
| 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
 | |
| 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 using
 | |
| :meth:`django.contrib.admin.ModelAdmin.message_user` to display a user friendly
 | |
| description of the problem in the response.
 | |
| 
 | |
| Advanced action techniques
 | |
| ==========================
 | |
| 
 | |
| There's a couple of extra options and possibilities you can exploit for more
 | |
| advanced options.
 | |
| 
 | |
| Actions as :class:`ModelAdmin` methods
 | |
| --------------------------------------
 | |
| 
 | |
| The example above shows the ``make_published`` action defined as a simple
 | |
| function. That's perfectly fine, but it's not perfect from a code design point
 | |
| of view: since the action is tightly coupled to the ``Article`` object, it
 | |
| makes sense to hook the action to the ``ArticleAdmin`` object itself.
 | |
| 
 | |
| That's easy enough to do::
 | |
| 
 | |
|     class ArticleAdmin(admin.ModelAdmin):
 | |
|         ...
 | |
| 
 | |
|         actions = ['make_published']
 | |
| 
 | |
|         def make_published(self, request, queryset):
 | |
|             queryset.update(status='p')
 | |
|         make_published.short_description = "Mark selected stories as published"
 | |
| 
 | |
| Notice first that we've moved ``make_published`` into a method and renamed the
 | |
| ``modeladmin`` parameter to ``self``, and second that we've now put the string
 | |
| ``'make_published'`` in ``actions`` instead of a direct function reference. This
 | |
| tells the :class:`ModelAdmin` to look up the action as a method.
 | |
| 
 | |
| Defining actions as methods gives the action more straightforward, idiomatic
 | |
| access to the :class:`ModelAdmin` itself, allowing the action to call any of the
 | |
| methods provided by the admin.
 | |
| 
 | |
| .. _custom-admin-action:
 | |
| 
 | |
| For example, we can use ``self`` to flash a message to the user informing her
 | |
| that the action was successful::
 | |
| 
 | |
|     class ArticleAdmin(admin.ModelAdmin):
 | |
|         ...
 | |
| 
 | |
|         def make_published(self, request, queryset):
 | |
|             rows_updated = queryset.update(status='p')
 | |
|             if rows_updated == 1:
 | |
|                 message_bit = "1 story was"
 | |
|             else:
 | |
|                 message_bit = "%s stories were" % rows_updated
 | |
|             self.message_user(request, "%s successfully marked as published." % message_bit)
 | |
| 
 | |
| This make the action match what the admin itself does after successfully
 | |
| performing an action:
 | |
| 
 | |
| .. image:: _images/article_actions_message.png
 | |
| 
 | |
| Actions that provide intermediate pages
 | |
| ---------------------------------------
 | |
| 
 | |
| By default, after an action is performed the user is simply redirected back
 | |
| to the original change list page. However, some actions, especially more
 | |
| complex ones, will need to return intermediate pages. For example, the
 | |
| built-in delete action asks for confirmation before deleting the selected
 | |
| objects.
 | |
| 
 | |
| To provide an intermediary page, simply return an
 | |
| :class:`~django.http.HttpResponse` (or subclass) from your action. For
 | |
| example, you might write a simple export function that uses Django's
 | |
| :doc:`serialization functions </topics/serialization>` to dump some selected
 | |
| objects as JSON::
 | |
| 
 | |
|     from django.http import HttpResponse
 | |
|     from django.core import serializers
 | |
| 
 | |
|     def export_as_json(modeladmin, request, queryset):
 | |
|         response = HttpResponse(content_type="application/json")
 | |
|         serializers.serialize("json", queryset, stream=response)
 | |
|         return response
 | |
| 
 | |
| Generally, something like the above isn't considered a great idea. Most of the
 | |
| time, the best practice will be to return an
 | |
| :class:`~django.http.HttpResponseRedirect` and redirect the user to a view
 | |
| you've written, passing the list of selected objects in the GET query string.
 | |
| This allows you to provide complex interaction logic on the intermediary
 | |
| pages. For example, if you wanted to provide a more complete export function,
 | |
| you'd want to let the user choose a format, and possibly a list of fields to
 | |
| include in the export. The best thing to do would be to write a small action
 | |
| that simply redirects to your custom export view::
 | |
| 
 | |
|     from django.contrib import admin
 | |
|     from django.contrib.contenttypes.models import ContentType
 | |
|     from django.http import HttpResponseRedirect
 | |
| 
 | |
|     def export_selected_objects(modeladmin, request, queryset):
 | |
|         selected = request.POST.getlist(admin.ACTION_CHECKBOX_NAME)
 | |
|         ct = ContentType.objects.get_for_model(queryset.model)
 | |
|         return HttpResponseRedirect("/export/?ct=%s&ids=%s" % (ct.pk, ",".join(selected)))
 | |
| 
 | |
| As you can see, the action is the simple part; all the complex logic would
 | |
| belong in your export view. This would need to deal with objects of any type,
 | |
| hence the business with the ``ContentType``.
 | |
| 
 | |
| Writing this view is left as an exercise to the reader.
 | |
| 
 | |
| .. _adminsite-actions:
 | |
| 
 | |
| Making actions available site-wide
 | |
| ----------------------------------
 | |
| 
 | |
| .. method:: AdminSite.add_action(action[, name])
 | |
| 
 | |
|     Some actions are best if they're made available to *any* object in the admin
 | |
|     site -- the export action defined above would be a good candidate. You can
 | |
|     make an action globally available using :meth:`AdminSite.add_action()`. For
 | |
|     example::
 | |
| 
 | |
|         from django.contrib import admin
 | |
| 
 | |
|         admin.site.add_action(export_selected_objects)
 | |
| 
 | |
|     This makes the ``export_selected_objects`` action globally available as an
 | |
|     action named "export_selected_objects". You can explicitly give the action
 | |
|     a name -- good if you later want to programmatically :ref:`remove the action
 | |
|     <disabling-admin-actions>` -- by passing a second argument to
 | |
|     :meth:`AdminSite.add_action()`::
 | |
| 
 | |
|         admin.site.add_action(export_selected_objects, 'export_selected')
 | |
| 
 | |
| .. _disabling-admin-actions:
 | |
| 
 | |
| Disabling actions
 | |
| -----------------
 | |
| 
 | |
| Sometimes you need to disable certain actions -- especially those
 | |
| :ref:`registered site-wide <adminsite-actions>` -- for particular objects.
 | |
| There's a few ways you can disable actions:
 | |
| 
 | |
| Disabling a site-wide action
 | |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
| .. method:: AdminSite.disable_action(name)
 | |
| 
 | |
|     If you need to disable a :ref:`site-wide action <adminsite-actions>` you can
 | |
|     call :meth:`AdminSite.disable_action()`.
 | |
| 
 | |
|     For example, you can use this method to remove the built-in "delete selected
 | |
|     objects" action::
 | |
| 
 | |
|         admin.site.disable_action('delete_selected')
 | |
| 
 | |
|     Once you've done the above, that action will no longer be available
 | |
|     site-wide.
 | |
| 
 | |
|     If, however, you need to re-enable a globally-disabled action for one
 | |
|     particular model, simply list it explicitly in your ``ModelAdmin.actions``
 | |
|     list::
 | |
| 
 | |
|         # Globally disable delete selected
 | |
|         admin.site.disable_action('delete_selected')
 | |
| 
 | |
|         # This ModelAdmin will not have delete_selected available
 | |
|         class SomeModelAdmin(admin.ModelAdmin):
 | |
|             actions = ['some_other_action']
 | |
|             ...
 | |
| 
 | |
|         # This one will
 | |
|         class AnotherModelAdmin(admin.ModelAdmin):
 | |
|             actions = ['delete_selected', 'a_third_action']
 | |
|             ...
 | |
| 
 | |
| 
 | |
| Disabling all actions for a particular :class:`ModelAdmin`
 | |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
| If you want *no* bulk actions available for a given :class:`ModelAdmin`, simply
 | |
| set :attr:`ModelAdmin.actions` to ``None``::
 | |
| 
 | |
|     class MyModelAdmin(admin.ModelAdmin):
 | |
|         actions = None
 | |
| 
 | |
| This tells the :class:`ModelAdmin` to not display or allow any actions,
 | |
| including any :ref:`site-wide actions <adminsite-actions>`.
 | |
| 
 | |
| Conditionally enabling or disabling actions
 | |
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
| .. method:: ModelAdmin.get_actions(request)
 | |
| 
 | |
|     Finally, you can conditionally enable or disable actions on a per-request
 | |
|     (and hence per-user basis) by overriding :meth:`ModelAdmin.get_actions`.
 | |
| 
 | |
|     This returns a dictionary of actions allowed. The keys are action names, and
 | |
|     the values are ``(function, name, short_description)`` tuples.
 | |
| 
 | |
|     Most of the time you'll use this method to conditionally remove actions from
 | |
|     the list gathered by the superclass. For example, if I only wanted users
 | |
|     whose names begin with 'J' to be able to delete objects in bulk, I could do
 | |
|     the following::
 | |
| 
 | |
|         class MyModelAdmin(admin.ModelAdmin):
 | |
|             ...
 | |
| 
 | |
|             def get_actions(self, request):
 | |
|                 actions = super(MyModelAdmin, self).get_actions(request)
 | |
|                 if request.user.username[0].upper() != 'J':
 | |
|                     if 'delete_selected' in actions:
 | |
|                         del actions['delete_selected']
 | |
|                 return actions
 |