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

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

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

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

../../../_images/user_actions.png

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

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

Если вам потребуется изменить такое поведение, то просто напишите собственное действие, которое выполняет удаление в необходимой вам манере, например, вызывая 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):              # __unicode__ on Python 2
        return self.title

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

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

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

  • Экземпляр класса ModelAdmin,

  • Экземпляр класса HttpRequest, представляющий текущий запрос,

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

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

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

Примечание

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

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/article_actions.png

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

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

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

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

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

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

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

Это достаточно просто сделать:

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 искать действие среди своих методов.

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

Например, мы можем использовать 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/article_actions_message.png

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

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

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

from django.http import HttpResponse
from django.core import serializers

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

Как вы можете видеть, код самого действия очень прост. Вся сложная логика будет располагаться в вашем представлении экспорта. Здесь потребуется работать с объектами любого типа, следовательно тут надо работать через ContentType.

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

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

AdminSite.add_action(action[, name])

Некоторые действия настолько хороши, что их следует сделать доступными для любого объекта в интерфейсе администратора. Действие экспорта, определённое выше, будет хорошим кандидатом для этого. Вы можете сделать действие видимым глобально, воспользуйтесь методом 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')

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

Тем не менее, если вам потребуется вернуть глобально отключенное действия для одной конкретной модели, просто укажите это действия явно в списке 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(MyModelAdmin, self).get_actions(request)
        if request.user.username[0].upper() != 'J':
            if 'delete_selected' in actions:
                del actions['delete_selected']
        return actions