Действия администратора

Повседневный алгоритм работы с административным интерфейсом Django выглядит как «выделить объект, затем изменить его.» Он подходит для большинства случаев. Тем не менее, когда потребуется выполнить одно и то же действие над множеством объектов, то такое поведение интерфейса начинает напрягать.

In these cases, Django’s admin lets you write and register «actions» – functions that get called with a list of objects selected on the change list page.

Если вы взгляните на любой список изменений на интерфейсе администратора, вы увидите эту возможность в действии. Django поставляется с действием «удалить выделенные объекты», которое доступно для всех моделей. Например, рассмотрим пользовательский модуль из встроенного в Django приложения django.contrib.auth:

../../../_images/admin-actions.png

Предупреждение

Действие «удалить выделенные объекты» использует метод QuerySet.delete() по соображениям эффективности, который имеет важный недостаток: метод delete() вашей модели не будет вызван.

If you wish to override this behavior, you can override 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.

Подробности по пакетному удалению смотрите в документации по удалению объектов.

Читайте дальше о том, как создавать собственные действия для списка объектов.

Создание действий

Простейшим способом понять работу действий является их изучение на примерах. Значит, пришло время изучить их внимательнее.

A common use case for admin actions is the bulk updating of a model. Imagine a 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

Стандартной задачей, которую мы возможно будем выполнять с подобной моделью, будет изменение состояний статьи с «черновик» на «опубликовано». Мы легко сможем выполнить это действие в интерфейсе администратора для одной статьи за раз, но если потребуется выполнить массовую публикацию группы статей, то вы столкнётесь с нудной работой. Таким образом, следует написать действие, которое позволит нам изменять состояние статьи на «опубликовано.»

Создание функций для действий

First, we’ll need to write a function that gets called when the action is triggered from the admin. Action functions are regular functions that take three arguments:

  • Экземпляр класса ModelAdmin,
  • Экземпляр класса HttpRequest, представляющий текущий запрос,
  • Экземпляр класса QuerySet, содержащий набор объектов, которые выделил пользователь.

Наша функция «опубликовать-эти-статьи» не нуждается в экземпляре ModelAdmin или в объекте реквеста, но использует выборку:

def make_published(modeladmin, request, queryset):
    queryset.update(status='p')

Примечание

For the best performance, we’re using the queryset’s update method. Other types of actions might need to deal with each object individually; in these cases we’d iterate over the queryset:

for obj in queryset:
    do_something_with(obj)

В общем-то мы рассмотрели всё, что требуется для создания действия» Однако, мы сделаем ещё один необязательный, но полезный шаг и обеспечим действие «красивым» заголовком, который будет отображаться в интерфейсе администратора. По умолчанию, это действие будет отображено в списке действий как «Make published», т.е. по имени функции, где символы подчёркивания будут заменены пробелами. Неплохо, но мы можем сделать лучше, по человечески, предоставив функции make_published атрибут short_description:

def make_published(modeladmin, request, queryset):
    queryset.update(status='p')
make_published.short_description = "Mark selected stories as published"

Примечание

Вы уже встречались с этим. Опция list_display интерфейса администратора использует подобный подход для предоставления читаемых описаний для функций-обработчиков.

Добавление действий в класс ModelAdmin

Затем мы должны проинформировать наш класс ModelAdmin о новом действии. Это действие аналогично применению любой другой опции конфигурации. Таким образом, полный пример admin.py с определением действия и его регистрации будет выглядеть так:

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)

Этот код предоставит нам список моделей в интерфейсе администратора, который выгладит примерно так:

../../../_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 covers more advanced techniques.

Обработка ошибок в действиях

При наличии предполагаемых условий возникновения ошибки, которая может возникнуть во время работы вашего действия, вы должны аккуратно проинформировать пользователя о проблеме. Это подразумевает обработку исключений и использование метода django.contrib.admin.ModelAdmin.message_user() для отображения описания проблемы в отклике.

Продвинутые методики работы с действиями

Существует ряд дополнительных опций и возможностей, которые вы можете использовать в своём коде.

Действия как методы ModelAdmin

The example above shows the make_published action defined as a 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.

You can do it like this:

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"

Следует отметить, что сначала мы переместили make_published в метод и переименовали параметр modeladmin в self, а затем поместили строку make_published в атрибут actions вместо прямой ссылки на функцию. Всё это указывает классу ModelAdmin искать действие среди своих методов.

Defining actions as methods gives the action more idiomatic access to the ModelAdmin itself, allowing the action to call any of the methods provided by the admin.

Например, мы можем использовать self для вывода сообщения для пользователя в целях его информирования об успешном завершении действия:

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)

Это обеспечивает действие функционалом, аналогичным встроенным возможностям интерфейса администратора:

../../../_images/actions-as-modeladmin-methods.png

Действия, у которых есть промежуточные страницы

By default, after an action is performed the user is 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, return an HttpResponse (or subclass) from your action. For example, you might write a export function that uses Django’s serialization functions 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 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 redirects to your custom export view:

from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect

def export_selected_objects(modeladmin, request, queryset):
    selected = queryset.values_list('pk', flat=True)
    ct = ContentType.objects.get_for_model(queryset.model)
    return HttpResponseRedirect('/export/?ct=%s&ids=%s' % (
        ct.pk,
        ','.join(str(pk) for pk in selected),
    ))

As you can see, the action is rather short; 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.

Написание самого представления оставлено читателю в качестве домашнего задания.

Делаем действия видимыми всему сайту

AdminSite.add_action(action, name=None)

Некоторые действия настолько хороши, что их следует сделать доступными для любого объекта в интерфейсе администратора. Действие экспорта, определённое выше, будет хорошим кандидатом для этого. Вы можете сделать действие видимым глобально, воспользуйтесь методом AdminSite.add_action(). Например:

from django.contrib import admin

admin.site.add_action(export_selected_objects)

Действие export_selected_objects станет доступным глобально под именем «export_selected_objects». Вы можете явно дать имя этому действию, например, вам потребуется затем программно удалить действие, передав второй аргумент в метод AdminSite.add_action():

admin.site.add_action(export_selected_objects, 'export_selected')

Отключение действий

Иногда требуется отключать определённые действия, особенно зарегистрированные глобально, для определённых объектов. Существует несколько способов для этого:

Отключение глобального действия

AdminSite.disable_action(name)

Если требуется отключить глобальное действие, вы можете вызвать метод AdminSite.disable_action().

Например, вы можете использовать данный метод для удаления встроенного действия «delete selected objects»:

admin.site.disable_action('delete_selected')

После этого действие больше не будет доступно глобально.

If, however, you need to re-enable a globally-disabled action for one particular model, 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']
    ...

Отключение всех действия для определённого экземпляра ModelAdmin

If you want no bulk actions available for a given ModelAdmin, set ModelAdmin.actions to None:

class MyModelAdmin(admin.ModelAdmin):
    actions = None

Это укажет экземпляру ModelAdmin не показывать и не позволять выполнения никаких действий, включая зарегистрированные глобально.

Условное включение и отключение действий

ModelAdmin.get_actions(request)

Наконец, вы можете включать или отключать действия по некоему условию на уровне запроса (и, следовательно, на уровне каждого пользователя), просто переопределив метод ModelAdmin.get_actions().

Он возвращает словарь разрешённых действий. Ключами являются имена действий, а значениями являются кортежи вида (function, name, short_description).

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

Setting permissions for actions

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 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:

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))