管理員動作

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

我們可能會對這種模型執行的常見任務是將文章的狀態從「草稿」更新為「已發佈」。我們可以在管理介面中一次輕鬆完成一篇文章,但如果我們要批次發佈一群文章,這將會很繁瑣。所以,讓我們撰寫一個動作,讓我們將文章的狀態變更為「已發佈」。

撰寫動作函式

首先,我們需要撰寫一個函數,當從管理介面觸發動作時會呼叫該函數。動作函數是接受三個參數的常規函數

我們的發佈這些文章函數不需要 ModelAdmin 或請求物件,但我們將使用查詢集

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

注意

為了獲得最佳效能,我們使用查詢集的 更新方法。其他類型的動作可能需要單獨處理每個物件;在這些情況下,我們會疊代查詢集

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

一旦您完成以上步驟,該動作將不再於全網站可用。

然而,如果您需要為特定模型重新啟用一個全域停用的動作,請在您的 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(),根據每個請求(因此也根據每個使用者)有條件地啟用或停用動作。

這會回傳一個允許的動作字典。鍵是動作名稱,而值是 (函數, 名稱, 簡短描述) 元組。

例如,如果您只希望名稱以「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)

在這種情況下,它不會為函數添加任何屬性。

動作描述是經過 % 格式化的,可能包含 '%(verbose_name)s''%(verbose_name_plural)s' 佔位符,它們會分別被模型的 verbose_nameverbose_name_plural 取代。

回到頂部