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

Основной рабочий процесс администратора 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)

Вот, собственно, и все, что нужно для написания действия! Однако мы сделаем еще один необязательный, но полезный шаг и дадим действию «красивое» название в админке. По умолчанию это действие будет отображаться в списке действий как «Сделать опубликованным» - название функции с подчеркиваниями, замененными пробелами. Это хорошо, но мы можем дать лучшее, более удобное для человека название, используя декоратор action() на функции make_published:

from django.contrib import admin

...

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

Примечание

Это может показаться знакомым; опция администратора list_display использует аналогичную технику с декоратором display(), чтобы предоставить человекочитаемые описания для функций обратного вызова, зарегистрированных там же.

Changed in Django 3.2:

Аргумент description в декораторе action() эквивалентен установке атрибута short_description непосредственно в функции действия в предыдущих версиях. Установка атрибута напрямую по-прежнему поддерживается для обратной совместимости.

Добавление действий в 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, позволяя действию вызывать любой из методов, предоставляемых администратором.

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

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

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

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

Чтобы обеспечить промежуточную страницу, верните HttpResponse (или подкласс) из вашего действия. Например, вы можете написать функцию экспорта, которая использует serialization functions от Django, чтобы выгрузить некоторые выбранные объекты в виде 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')

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

Если, однако, вам нужно снова включить глобально отключенное действие для одной конкретной модели, перечислите его в явном виде в списке ModelAdmin.actions:

# 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

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

Действия могут ограничить свою доступность для пользователей с определенными правами, обернув функцию действия декоратором action() и передав аргумент permissions:

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

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

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

Доступными значениями для permissions и соответствующих проверок метода являются:

Вы можете указать любое другое значение, если вы реализуете соответствующий метод 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))
Changed in Django 3.2:

Аргумент permissions в декораторе action() эквивалентен установке атрибута allowed_permissions непосредственно в функции действия в предыдущих версиях. Установка атрибута напрямую по-прежнему поддерживается для обратной совместимости.

Декоратор action

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

Этот декоратор можно использовать для установки определенных атрибутов для пользовательских функций действия, которые могут быть использованы с 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)

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

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