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

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

В этих случаях админка Django позволяет писать и регистрировать «действия» - функции, которые вызываются с помощью списка объектов, выбранных на странице списка изменений.

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

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

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

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

Если вы хотите отменить это поведение, вы можете переопределить ModelAdmin.delete_queryset() или написать пользовательское действие, которое будет выполнять удаление выбранным вами способом - например, вызывая Model.delete() для каждого из выбранных элементов.

Более подробную информацию о массовом удалении см. в документации по object deletion.

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

Действия при написании

Проще всего объяснить действия на примере, поэтому давайте погрузимся в процесс.

Частым случаем использования действий администратора является массовое обновление модели. Представьте себе новостное приложение с моделью Article:

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

Обычная задача, которую мы можем выполнить с помощью подобной модели, - обновить статус статьи с «черновика» до «опубликованной». Мы можем легко сделать это в админке по одной статье за раз, но если мы хотим массово опубликовать группу статей, это будет утомительно. Поэтому давайте напишем действие, которое позволит нам изменить статус статьи на «опубликовано».

Написание функций действия

Во-первых, нам нужно написать функцию, которая будет вызываться, когда действие запускается из админки. Функции действия - это обычные функции, принимающие три аргумента:

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

Нашей функции publish-these-articles не понадобится ModelAdmin или объект request, но мы будем использовать queryset:

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

Примечание

Для наилучшей производительности мы используем update method queryset. В других типах действий может потребоваться работать с каждым объектом отдельно; в этих случаях мы будем выполнять итерации по набору 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 using the action() decorator on the make_published function:

from django.contrib import admin

...

@admin.action(description='Mark selected stories as published')
def make_published(modeladmin, request, queryset):
    queryset.update(status='p')

Примечание

This might look familiar; the admin’s list_display option uses a similar technique with the display() decorator to provide human-readable descriptions for callback functions registered there, too.

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

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

from django.contrib import admin
from myapp.models import Article

@admin.action(description='Mark selected stories as published')
def make_published(modeladmin, request, queryset):
    queryset.update(status='p')

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

Вот, собственно, и все! Если вам не терпится написать свои собственные действия, вы теперь знаете достаточно, чтобы начать. Остальная часть этого документа посвящена более продвинутым техникам.

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

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

Продвинутые техники действий

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

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

В приведенном выше примере действие make_published определено как функция. Это вполне нормально, но не идеально с точки зрения дизайна кода: поскольку действие тесно связано с объектом Article, имеет смысл подключить действие к самому объекту ArticleAdmin.

Вы можете сделать это следующим образом:

class ArticleAdmin(admin.ModelAdmin):
    ...

    actions = ['make_published']

    @admin.action(description='Mark selected stories as published')
    def make_published(self, request, queryset):
        queryset.update(status='p')

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

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

For example, we can use self to flash a message to the user informing them that the action was successful:

from django.contrib import messages
from django.utils.translation import ngettext

class ArticleAdmin(admin.ModelAdmin):
    ...

    def make_published(self, request, queryset):
        updated = queryset.update(status='p')
        self.message_user(request, ngettext(
            '%d story was successfully marked as published.',
            '%d stories were successfully marked as published.',
            updated,
        ) % updated, messages.SUCCESS)

Это делает действие соответствующим тому, что делает сам администратор после успешного выполнения действия:

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

Действия, предоставляющие промежуточные страницы

По умолчанию после выполнения действия пользователь перенаправляется обратно на исходную страницу списка изменений. Однако некоторые действия, особенно более сложные, должны возвращать промежуточные страницы. Например, встроенное действие удаления запрашивает подтверждение перед удалением выбранных объектов.

To provide an intermediary page, return an HttpResponse (or subclass) from your action. For example, you might write an 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

Как правило, что-то вроде вышеописанного не считается хорошей идеей. В большинстве случаев лучшей практикой будет возвращать HttpResponseRedirect и перенаправлять пользователя на написанное вами представление, передавая список выбранных объектов в строке запроса GET. Это позволит вам обеспечить сложную логику взаимодействия на промежуточных страницах. Например, если вы хотите предоставить более полную функцию экспорта, вы должны позволить пользователю выбрать формат и, возможно, список полей, которые будут включены в экспорт. Лучше всего написать небольшое действие, перенаправляющее на пользовательский вид экспорта:

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

Как вы можете видеть, действие довольно короткое; вся сложная логика будет находиться в вашем экспортном представлении. Здесь нужно будет работать с объектами любого типа, отсюда и бизнес с 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». Вы можете явно дать действию имя - хорошо, если вы позже захотите программно remove the action> - передав второй аргумент в AdminSite.add_action():

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

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

Иногда вам нужно отключить определенные действия - особенно те, которые registered site-wide - для определенных объектов. Есть несколько способов отключить действия:

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

AdminSite.disable_action(name)[исходный код]

Если вам нужно отключить site-wide action, вы можете вызвать AdminSite.disable_action().

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

admin.site.disable_action('delete_selected')

После выполнения вышеуказанных действий это действие больше не будет доступно на всем сайте.

If, however, you need to reenable 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

Если вы хотите, чтобы для данного ModelAdmin не было доступно никаких массовых действий, установите ModelAdmin.actions в None:

class MyModelAdmin(admin.ModelAdmin):
    actions = None

Это говорит ModelAdmin не отображать и не разрешать никаких действий, включая любые site-wide actions.

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

ModelAdmin.get_actions(request)[исходный код]

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

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

Например, если вы хотите, чтобы только пользователи, чьи имена начинаются с „J“, могли массово удалять объекты:

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

Установка разрешений для действий

Actions may limit their availability to users with specific permissions by wrapping the action function with the action() decorator and passing the permissions argument:

@admin.action(permissions=['change'])
def make_published(modeladmin, request, queryset):
    queryset.update(status='p')

Действие make_published() будет доступно только тем пользователям, которые прошли проверку ModelAdmin.has_change_permission().

If 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 permissions and the corresponding method checks are:

Вы можете указать любое другое значение, если вы реализуете соответствующий метод has_<value>_permission(self, request) на ModelAdmin.

Например:

from django.contrib import admin
from django.contrib.auth import get_permission_codename

class ArticleAdmin(admin.ModelAdmin):
    actions = ['make_published']

    @admin.action(permissions=['publish'])
    def make_published(self, request, queryset):
        queryset.update(status='p')

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

Декоратор action

action(*, permissions=None, description=None)[исходный код]

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

@admin.action(
    permissions=['publish'],
    description='Mark selected stories as published',
)
def make_published(self, request, queryset):
    queryset.update(status='p')

Это эквивалентно установке некоторых атрибутов (с оригинальными, более длинными именами) непосредственно на функцию:

def make_published(self, request, queryset):
    queryset.update(status='p')
make_published.allowed_permissions = ['publish']
make_published.short_description = 'Mark selected stories as published'

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

@admin.action
def make_inactive(self, request, queryset):
    queryset.update(is_active=False)

В этом случае он не будет добавлять никаких атрибутов к функции.

Вернуться на верх