管理动作

Django 的管理员的基本工作流程,简而言之,就是“选择一个对象,然后更改它”。这对于大多数用例来说都很好用。然而,如果你需要同时对许多对象进行相同的更改,这种工作流程可能会相当乏味。

在这种情况下,Django 的管理可以让你编写和注册“动作”——即使用在变更列表页面上选择的对象列表调用的函数。

如果你在管理中查看任何变化列表,你会看到这个功能的作用;Django 自带了一个“删除选定对象”的动作,所有模型都可以使用。例如,这里是 Django 内置的用户模块 django.contrib.auth 应用:

../../../_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

我们可能会用这样的模型来执行一个常见的任务,就是将一篇文章的状态从 “草稿” 更新为 “已发布”。我们可以很容易地在管理员中一次只发布一篇文章,但如果我们想批量发布一组文章,就会很繁琐。所以,让我们编写一个动作,让我们可以把一篇文章的状态改为 “已发布”。

编写动作函数

首先,我们需要写一个函数,当管理触发动作时就会被调用。动作函数是常规函数,它有三个参数:

我们的 publish-these-articles 函数不需要 ModelAdmin 或请求对象,但我们将使用此查询集:

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

备注

为了获得最佳性能,我们使用查询集的 update 方法。其他类型的动作可能需要单独处理每个对象;在这种情况下,我们会在查询集上进行迭代:

for obj in queryset:
    do_something_with(obj)

其实这就是写一个动作的全部内容了!然而,我们将再采取一个可选的但很有用的步骤,在管理中给这个动作一个 “漂亮” 的标题。默认情况下,这个操作会在操作列表中显示为 “Make published” —— 函数名称,下划线用空格代替。这很好,但我们可以通过在 make_published 函数上使用 action() 装饰器来提供一个更好的、更人性化的名称:

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

一旦你完成了上述操作,该动作将不再在全站范围内使用。

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 不显示或允许任何操作,包括任何 全站点动作

有条件地启用或禁用动作

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 和相应的方法检查的可用值是:

你可以指定任何其他的值,只要你在 ModelAdmin 上实现一个相应的 has_<value>_permission(self, request) 方法。

例子:

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)

在这种情况下,它将不会给函数添加任何属性。

Action descriptions are %-formatted and may contain '%(verbose_name)s' and '%(verbose_name_plural)s' placeholders, which are replaced, respectively, by the model's verbose_name and verbose_name_plural.