Spec-Zone.ru › Django 6.0

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

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

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

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

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

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

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

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

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

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

Написание действий

Проще всего объяснить, что такое действия, на примере. Давайте перейдём к нему.

Один из распространённых случаев использования действий администратора — массовое обновление модели. Представим новостное приложение с моделью 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, содержащий набор объектов, выбранных пользователем.

Для нашей функции публикации статей не понадобятся ни ModelAdmin, ни объект запроса, но мы воспользуемся набором запросов:

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

Примечание

Для повышения производительности мы используем метод update набора запросов. Для действий других типов может потребоваться обработать каждый объект отдельно; в таких случаях мы перебирали бы набор запросов:

for obj in queryset:
    do_something_with(obj)

Вот и всё, что нужно для написания действия! Однако сделаем ещё один необязательный, но полезный шаг и зададим для действия «красивое» название в панели администратора. По умолчанию это действие появилось бы в списке под названием «Make published» — именем функции, в котором символы подчёркивания заменены пробелами. Это вполне допустимо, но можно задать более понятное название с помощью декоратора 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(), чтобы также задавать понятные описания для зарегистрированных там функций обратного вызова.

Добавление действий в 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 (или его подкласс). Например, можно написать функцию экспорта, которая использует функции сериализации 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». Можно явно задать имя действия — это удобно, если позже вы захотите программно удалить действие, — передав второй аргумент в AdminSite.add_action():

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

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

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

Отключение действия, доступного на всём сайте

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

Чтобы отключить действие, доступное на всём сайте, вызовите 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 не отображать и не разрешать никакие действия, в том числе действия, доступные на всём сайте.

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

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 и соответствующие проверки методов:

  • 'add': ModelAdmin.has_add_permission()
  • 'change': ModelAdmin.has_change_permission()
  • 'delete': ModelAdmin.has_delete_permission()
  • 'view': ModelAdmin.has_view_permission()

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

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

Описания действий форматируются с помощью оператора %, в них могут использоваться заполнители '%(verbose_name)s' и '%(verbose_name_plural)s', которые заменяются соответственно значениями verbose_name и verbose_name_plural модели.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/ref/contrib/admin/actions/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API