iT邦幫忙

2024 iThome 鐵人賽

DAY 8
0
Software Development

Django 2024: 從入門到SaaS實戰系列 第 9

Django in 2024: Django Admin二次開發,打造屬於你的後台

  • 分享至 

  • xImage
  •  

在昨天我們認識到Django Admin能夠快速滿足我們在後台所需要的基本需求,今天我們會繼續延伸探討還能怎麼修改Django Admin,讓營運人員在問你能不能添加某某功能時,能夠大膽的說沒問題!

今天的重點如下:

  • 改寫Admin方法,更彈性的處理資料與權限
  • 客製化Admin頁面
  • 新增自定義頁面到Admin中

程式碼:https://github.com/class83108/django_project/tree/admin

改寫Admin方法,更彈性的處理資料與權限

  • get_readonly_fields

先前有簡單帶過有關model權限的部分,我們再來簡單的看一下
https://ithelp.ithome.com.tw/upload/images/20240920/20161866F5RbqTb4RE.png

以文章model為例,我們可以設置用戶對於該model的增刪改查等權限

但是我們可能會想要把權限切的更細,把權限的層級放到model的欄位上呢?這時候可以透過改寫

get_readonly_fields 達成

先創建另一個用戶,並且只具備文章model與staff權限

https://ithelp.ithome.com.tw/upload/images/20240920/20161866tdOYg5qEqD.png
https://ithelp.ithome.com.tw/upload/images/20240920/20161866qxK5SNm5EV.png

我們的目標是想要讓cover這個欄位,在沒有超級用戶的權限時,只有讀的權限

class ArticleAdmin(admin.ModelAdmin):

    ...

    def get_readonly_fields(
        self, request: HttpRequest, obj: Any | None = ...
    ) -> list[str] | tuple[Any, ...]:
        if request.user.is_superuser:
            self.readonly_fields = []
        else:
            self.readonly_fields = ["cover"]
        return self.readonly_fields

接著我們開啟無痕瀏覽器,然後登入我們剛剛新建立的帳號,找到文章對象後點擊

https://ithelp.ithome.com.tw/upload/images/20240920/20161866T3fhM7DtOY.png

在封面欄位中,因為該使用者不是超級用戶,所以無法進行修改

  • get_queryset

那除了控制只讀權限之外,我們用剛剛判斷是否為超級用戶的方法也可以應用在其他方法上

get_queryset方法用來控制呈現模型數據,因為默認是全表呈現,我們可以透過ORM語法來控制

class ArticleAdmin(admin.ModelAdmin):
		...

    def get_readonly_fields(
        self, request: HttpRequest, obj: Any | None = ...
    ) -> list[str] | tuple[Any, ...]:
        if request.user.is_superuser:
            self.readonly_fields = []
        else:
            self.readonly_fields = ["cover"]
        return self.readonly_fields

    def get_queryset(self, request: HttpRequest) -> Any:
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs
        return qs.filter(article_id__lt=2)
  • save_model

之前在form介紹過的save方法,當然admin也能讓資料在儲存的時候做改變

class ArticleAdmin(admin.ModelAdmin):

    ...

    def save_model(
        self, request: HttpRequest, obj: Any, form: ModelForm, change: bool
    ) -> None:
        if change:
            article_title = form.cleaned_data["title"]
            pattern = r"(.*)\.v(\d+)$"
            match = re.match(pattern, article_title)

            if match:
                base_title = match.group(1)
                version = int(match.group(2))
                new_version = version + 1
                new_title = f"{base_title}.v{new_version}"
            else:
                new_title = f"{article_title}.v1"
        else:
            # 如果是新建文章,添加 .v1
            new_title = f"{form.cleaned_data['title']}.v1"

        # 更新文章標題
        obj.title = new_title
        super().save_model(request, obj, form, change)

透過form.cleaned_data拿到特定欄位的數據,最後再將處理好的數據儲存到對象中

其中change用來判斷這次是更新還是新增

上面展示的方法,先判斷文章標題有沒有附上版本,沒有的話就附上,反之則將版本號+=1

  • action:資料批量操作

可以在列表頁看到目前只有預設的批量刪除操作
https://ithelp.ithome.com.tw/upload/images/20240920/20161866sq2m9MeobY.png

我們也能自定義這邊的操作

class ArticleAdmin(admin.ModelAdmin):
		...
    def add_version(self, request: HttpRequest, queryset: Any) -> None:
        for article in queryset:
            article_title = article.title
            pattern = r"(.*)\.v(\d+)$"
            match = re.match(pattern, article_title)

            if not match:
                new_title = f"{article_title}.v1"
                article.title = new_title
                article.save()
        self.message_user(request, "Add version successfully")

    add_version.short_description = "Add version"
    actions = ["add_version"]

我們一樣檢查文章標題有沒有版本號,沒有的話就要添加

  • queryset代表在這個頁面中所有選到的對象
  • message_user最後配合模板來顯示如果操作成功的資訊
  • add_version.short_desctiption為頁面上顯示的名稱
  • 最後透過actions將我們自定義的操作添加上去

https://ithelp.ithome.com.tw/upload/images/20240920/20161866uazTgOgEoE.png
https://ithelp.ithome.com.tw/upload/images/20240920/20161866ezXWJOBR0T.png

客製化Admin頁面

我們可能不一定滿意現在admin頁面中提供的資訊,或是想要添加我們自定義的css,或是js檔

我們能透過兩種方法來進行調整

  1. 改寫admin的模板
  2. 透過admin.py註冊的對象來添加屬性

改寫admin模板

以修改對象的頁面為例,可以看到該模板是在admin下的change_form.html

https://ithelp.ithome.com.tw/upload/images/20240920/201618668KkxkN6eKf.png

因此我們可以在我們的資料夾新增該檔案

https://ithelp.ithome.com.tw/upload/images/20240920/20161866qScZs3DJuU.png

{% extends "admin/change_form.html" %}

{% comment %} 自定義額外的css {% endcomment %}
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" href="{% static "admin/css/forms.css" %}">{% endblock %}

{% comment %} 可以添加額外的css或是js進去 {% endcomment %}
{% block extrahead %}{% endblock %}

{% comment %} 自定義內容 {% endcomment %}
{% block content %}

{% endblock content %}

不過js的部分要注意會不會造成太久的阻塞,或是可以塞在block content

透過admin.py註冊的對象來添加屬性

我們假如想讓tag的多選欄位,改成select元素的呈現方式:

class ArticleAdmin(admin.ModelAdmin):

    ...

    class Media:
        css = {
            "all": (
                "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css",
            )
        }

        js = (
            "https://code.jquery.com/jquery-3.6.0.min.js",
            "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",
            "js/init_select2.js",
        )

這邊選擇引入select2是第三方的js函式庫,主要是對select元素進行條整,官方文檔如下

https://select2.org/

使用時要先引入jquery,並且要設定好需要調用select2的元素

因此得自定義init_select2.js

$(document).ready(function () {
  $("#id_tags").select2();
});

可以看到的確套用了

https://ithelp.ithome.com.tw/upload/images/20240920/201618660BhWjZ7TEP.png

新增自定義頁面到Admin中

因為後台可能有時候不只是需要以模型為基準的功能,可能會需要額外的功能,此時我們就需要有辦法自定義自己的頁面,並且讓後台出現能夠點選的連結

要達成這件事情需要思考幾個面向:

  1. 如果是透過自定義的路由以及視圖,搭配上繼承的模板是可以的嗎?
  2. 既然都配對好路由了,要怎麼讓admin頁面中出現這個連結?

為了解決第一個問題,我們先去頁面上看可能需要的資料有哪些
https://ithelp.ithome.com.tw/upload/images/20240920/20161866BQNkY7jPDF.png

除了上方navbar中有一些連結之外,我們還可以看到sidebar中有那些註冊的app以及model

那我們要怎麼拿到這些資料?

此時我們先回去看admin中的模板

首先是base.html

https://ithelp.ithome.com.tw/upload/images/20240920/20161866ARhSLpnc9T.png

進到nav_sidebar.html

https://ithelp.ithome.com.tw/upload/images/20240920/20161866Nio9exWgml.png

最後看到app_list.html

https://ithelp.ithome.com.tw/upload/images/20240920/201618668W03pls9lp.png

我們可以看到我們需要app_list的資料

此時我們可以選擇生出app_list,但是除了要自己生資料出來之外,還必須要處理上面許多的模板標籤。因此我們可以透過重新製作一個Admin的後台,並且繼承基礎的資料來做

  • 首先在article.views下建立相關的視圖類別
from django.views.generic import TemplateView

class CustomAdminPageView(TemplateView):
    template_name = "admin/custom_admin_page.html"
    admin_site = None  # 初始化 admin_site

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)  # 獲取原始的上下文數據

        # 獲取傳遞的 AdminSite 實例
        admin_site = self.admin_site or self.request.site

        if admin_site:
            # 獲取完整的 admin 上下文
            admin_context = admin_site.each_context(self.request)
            context.update(admin_context)

            # 添加額外的上下文數據
            context.update(
                {
                    "title": "Custom Admin Page",
                    "subtitle": None,
                    # 是否為 popup
                    "is_popup": False,
                    "has_permission": self.request.user.is_active
                    and self.request.user.is_staff,
                    # 是否啟用側邊欄
                    "is_nav_sidebar_enabled": admin_context.get(
                        "is_nav_sidebar_enabled", True
                    ),
                    # 獲取應用列表
                    "available_apps": admin_site.get_app_list(self.request),
                    ERROR_FLAG: admin_context.get(ERROR_FLAG, ""),
                }
            )

        return context

    # 重寫as_view方法,保存添加admin_site屬性
    @classmethod
    def as_view(cls, **initkwargs):
        view = super().as_view(**initkwargs)
        view.admin_site = initkwargs.get("admin_site")
        return view

TemplateView的功能就是拿到keyword arguments後載入context(上下文)然後渲染到模板中

這邊的context,就像是我們之前在視圖定義的各式變數,最終會被模板解析並呈現

而在我們自定義的類別中,我們繼承了TemplateView,並且透過改寫get_context_data

  1. 拿到基礎admin_site中的上下文,先不管admin_site要怎麼拿,但是這裡的admin_site也就是admin.AdminSite對象,相當於是Django預設的後台對象
  2. 我們想要從admin.AdminSite上拿到基礎的app_list
  3. 把我們的自定義頁面添加到app_list中,這樣最後sidebar才能出現連結

最後重寫as_view方法,因為是類方法,所以能確保admin_site能夠被在視圖被創建時就附在上面

  • 此時我們去article下的admin.py
from django.contrib import admin
from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import GroupAdmin, UserAdmin

from article.views import CustomAdminPageView as CustomView

class CustomAdminPageView(admin.AdminSite):
    # 自定義 admin 頁面 除了原本的 urls 之外,再添加一個自定義的 url
    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path(
                "custom_admin_page/",
                self.admin_view(CustomView.as_view(admin_site=self)),
                name="custom_admin_page",
            ),
        ]
        return custom_urls + urls

    # 自定義 admin 頁面的左側導航欄
    def get_app_list(self, request: WSGIRequest) -> list[Any]:
        app_list = super().get_app_list(request)

        custom_admin_url = reverse("admin:custom_admin_page")

        app_list.append(
            {
                "name": _("Custom Admin Page"),
                "app_label": "custom_admin_page",
                "app_url": "",
                "has_module_perms": True,
                "models": [
                    {
                        "name": _("Custom Admin Page"),
                        "object_name": "CustomAdminPage",
                        "admin_url": custom_admin_url,
                    }
                ],
            }
        )
        return app_list
        
admin_site = CustomAdminPageView(name="admin")

admin_site.register(ArticleV2, ArticleAdmin)
admin_site.register(Tag)
admin_site.register(Category)
admin_site.register(Author, AuthorAdmin)
admin_site.register(Group, GroupAdmin)
admin_site.register(User, UserAdmin)

此時我們要做的,就是透過繼承admin.AdminSite做出自己的admin

  1. get_urls中,我們要將客製化的路由還有視圖加入
  2. self.admin_view(CustomView.as_view(admin_site=self))中有幾個重要的訊息
    • 可以看到調用了我們在視圖定義的類別,並且使用as_view來把這個admin本身傳過去
    • admin_view的源碼如下,他可以確保用戶登入,權限檢查等等的問題
  3. 最後添加到app_list中,並且將其他model進行註冊
def admin_view(self, view, cacheable=False):
        """
        Decorator to create an admin view attached to this ``AdminSite``. This
        wraps the view and provides permission checking by calling
        ``self.has_permission``.

        You'll want to use this from within ``AdminSite.get_urls()``:

            class MyAdminSite(AdminSite):

                def get_urls(self):
                    from django.urls import path

                    urls = super().get_urls()
                    urls += [
                        path('my_view/', self.admin_view(some_view))
                    ]
                    return urls

        By default, admin_views are marked non-cacheable using the
        ``never_cache`` decorator. If the view can be safely cached, set
        cacheable=True.
        """
  • 進行模板的配置

到templates/admin/下建立我們的檔案custom_admin_page.html,這在view中的屬性已經定義好了,TemplateView會幫忙返回,所以不用再寫什麼render(xxxx)

{% extends "admin/base_site.html" %}

{% block content %}
<p>This is your custom admin page content.</p>
{% endblock %}
  • 最後配置路由,我們要去根目錄,取代掉原本的路徑
from article.admin import admin_site

urlpatterns = [
    # path("admin/", admin.site.urls),
    path("admin/", admin_site.urls),
	 .....   
]

此時就大功告成了!如果想要用form表單的話,可以將TemplateView改成FormView
https://ithelp.ithome.com.tw/upload/images/20240920/20161866TBtaTGVDZ4.png
https://ithelp.ithome.com.tw/upload/images/20240920/20161866O7hjm8yTkJ.png
如果覺得上面說明得不夠清楚,我有請claude幫忙生出sequenceDiagram
https://ithelp.ithome.com.tw/upload/images/20240920/20161866xUT4yzwGdo.png
希望能讓讀者更清楚整個流程

今日總結

  • 透過修改admin中的方法,來處理資料在後台呈現的方式,或是儲存進DB之前的操作
  • 添加admin中的元數據,或是透過模板繼承的方式來客製化css與js
  • 最後改寫整個admin網站,來添加我們客製化的頁面與連結

今天我們又更深入的探討要怎麼客製化我們的後台網站,尤其是最後的部分

讓我們對於後台的掌握度又level up了!但是同時我們也可以發現,Django雖然封裝好了相當多的功能,但是要進行改寫或是debug時,反而會因為過度封裝而需要更了解內部運作的原理造成難度上升

明天我們還是會針對後台做文章,我們來看一下跟後台相關的一些第三方庫!


上一篇
Django in 2024: 強大的Django Admin
下一篇
Django in 2024: 那些你可能會想在後台使用的第三方庫
系列文
Django 2024: 從入門到SaaS實戰16
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言