mirror of
				https://github.com/django/django.git
				synced 2025-11-04 05:35:37 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			406 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
			
		
		
	
	
			406 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Text
		
	
	
	
	
	
=============
 | 
						|
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/admin-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, you can override
 | 
						|
    :meth:`.ModelAdmin.delete_queryset` or write a custom action which does
 | 
						|
    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):
 | 
						|
            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
 | 
						|
triggered 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/adding-actions-to-the-modeladmin.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/actions-as-modeladmin-methods.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.core import serializers
 | 
						|
    from django.http import HttpResponse
 | 
						|
 | 
						|
    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=None)
 | 
						|
 | 
						|
    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.
 | 
						|
 | 
						|
    For example, if you only want users whose names begin with 'J' to be able
 | 
						|
    to delete objects in bulk::
 | 
						|
 | 
						|
        class MyModelAdmin(admin.ModelAdmin):
 | 
						|
            ...
 | 
						|
 | 
						|
            def get_actions(self, request):
 | 
						|
                actions = super().get_actions(request)
 | 
						|
                if request.user.username[0].upper() != 'J':
 | 
						|
                    if 'delete_selected' in actions:
 | 
						|
                        del actions['delete_selected']
 | 
						|
                return actions
 | 
						|
 | 
						|
.. _admin-action-permissions:
 | 
						|
 | 
						|
Setting permissions for actions
 | 
						|
-------------------------------
 | 
						|
 | 
						|
.. versionadded:: 2.1
 | 
						|
 | 
						|
Actions may limit their availability to users with specific permissions by
 | 
						|
setting an ``allowed_permissions`` attribute on the action function::
 | 
						|
 | 
						|
    def make_published(modeladmin, request, queryset):
 | 
						|
        queryset.update(status='p')
 | 
						|
    make_published.allowed_permissions = ('change',)
 | 
						|
 | 
						|
The ``make_published()`` action will only be available to users that pass the
 | 
						|
:meth:`.ModelAdmin.has_change_permission` check.
 | 
						|
 | 
						|
If ``allowed_permissions`` has more than one permission, the action will be
 | 
						|
available as long as the user passes at least one of the checks.
 | 
						|
 | 
						|
Available values for ``allowed_permissions`` and the corresponding method
 | 
						|
checks are:
 | 
						|
 | 
						|
- ``'add'``: :meth:`.ModelAdmin.has_add_permission`
 | 
						|
- ``'change'``: :meth:`.ModelAdmin.has_change_permission`
 | 
						|
- ``'delete'``: :meth:`.ModelAdmin.has_delete_permission`
 | 
						|
- ``'view'``: :meth:`.ModelAdmin.has_view_permission`
 | 
						|
 | 
						|
You can specify any other value as long as you implement a corresponding
 | 
						|
``has_<value>_permission(self, request)`` method on the ``ModelAdmin``.
 | 
						|
 | 
						|
For example::
 | 
						|
 | 
						|
    from django.contrib import admin
 | 
						|
    from django.contrib.auth import get_permission_codename
 | 
						|
 | 
						|
    class ArticleAdmin(admin.ModelAdmin):
 | 
						|
        actions = ['make_published']
 | 
						|
 | 
						|
        def make_published(self, request, queryset):
 | 
						|
            queryset.update(status='p')
 | 
						|
        make_published.allowed_permissions = ('publish',)
 | 
						|
 | 
						|
        def has_publish_permission(self, request):
 | 
						|
            """Does the user have the publish permission?"""
 | 
						|
            opts = self.opts
 | 
						|
            codename = get_permission_codename('publish', opts)
 | 
						|
            return request.user.has_perm('%s.%s' % (opts.app_label, codename))
 |