Повседневный алгоритм работы с административным интерфейсом Django выглядит как “выделить объект, затем изменить его.” Он подходит для большинства случаев. Тем не менее, когда потребуется выполнить одно и то же действие над множеством объектов, то такое поведение интерфейса начинает напрягать.
В таких случаях административный интерфейс Django позволяет вам создать и зарегистрировать “действия” – простые функции, которые вызываются для выполнения неких действий над списком объектов, выделенных на странице интерфейса.
Если вы взгляните на любой список изменений на интерфейсе администратора, вы увидите эту возможность в действии. Django поставляется с действием “удалить выделенные объекты”, которое доступно для всех моделей. Например, рассмотрим пользовательский модуль из встроенного в Django приложения django.contrib.auth:
Предупреждение
Действие “удалить выделенные объекты” использует метод 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 о новом действии. Это действие аналогично применению любой другой опции конфигурации. Таким образом, полный пример 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)
Этот код предоставит нам список моделей в интерфейсе администратора, который выгладит примерно так:
Вот и всё! Если вам хочется поскорее создать свои действия, приступайте, у вас есть необходимые знания. Остальная часть документа просто описывает более продвинутые вещи.
При наличии предполагаемых условий возникновения ошибки, которая может возникнуть во время работы вашего действия, вы должны аккуратно проинформировать пользователя о проблеме. Это подразумевает обработку исключений и использование метода django.contrib.admin.ModelAdmin.message_user() для отображения описания проблемы в отклике.
Существует ряд дополнительных опций и возможностей, которые вы можете использовать в своём коде.
Вышеприведённый пример показывает действие 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)
Это обеспечивает действие функционалом, аналогичным встроенным возможностям интерфейса администратора:
По умолчанию, после выполнения действия пользователя просто перебрасывают обратно на страницу со списком объектов. Однако, некоторые действия, обычно наиболее сложные из них, могут отображать промежуточные страницы. Например, встроенное действие по удалению запрашивает подтверждение перед выполнением удаления выделенных объектов.
Чтобы предоставить такую промежуточную страницу следует просто вернуть объект класса 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(). Например:
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().
Например, вы можете использовать данный метод для удаления встроенного действия “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.actions в None:
class MyModelAdmin(admin.ModelAdmin):
actions = None
Это укажет экземпляру ModelAdmin не показывать и не позволять выполнения никаких действий, включая зарегистрированные глобально.
Наконец, вы можете включать или отключать действия по некоему условию на уровне запроса (и, следовательно, на уровне каждого пользователя), просто переопределив метод 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
Jun 02, 2016