在昨天我們認識到Django Admin能夠快速滿足我們在後台所需要的基本需求,今天我們會繼續延伸探討還能怎麼修改Django Admin,讓營運人員在問你能不能添加某某功能時,能夠大膽的說沒問題!
今天的重點如下:
程式碼:https://github.com/class83108/django_project/tree/admin
get_readonly_fields
先前有簡單帶過有關model權限的部分,我們再來簡單的看一下
以文章model為例,我們可以設置用戶對於該model的增刪改查等權限
但是我們可能會想要把權限切的更細,把權限的層級放到model的欄位上呢?這時候可以透過改寫
get_readonly_fields
達成
先創建另一個用戶,並且只具備文章model與staff權限
我們的目標是想要讓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
接著我們開啟無痕瀏覽器,然後登入我們剛剛新建立的帳號,找到文章對象後點擊
在封面欄位中,因為該使用者不是超級用戶,所以無法進行修改
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
可以在列表頁看到目前只有預設的批量刪除操作
我們也能自定義這邊的操作
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
為頁面上顯示的名稱我們可能不一定滿意現在admin頁面中提供的資訊,或是想要添加我們自定義的css,或是js檔
我們能透過兩種方法來進行調整
以修改對象的頁面為例,可以看到該模板是在admin下的change_form.html
因此我們可以在我們的資料夾新增該檔案
{% 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
中
我們假如想讓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元素進行條整,官方文檔如下
使用時要先引入jquery,並且要設定好需要調用select2的元素
因此得自定義init_select2.js
檔
$(document).ready(function () {
$("#id_tags").select2();
});
可以看到的確套用了
因為後台可能有時候不只是需要以模型為基準的功能,可能會需要額外的功能,此時我們就需要有辦法自定義自己的頁面,並且讓後台出現能夠點選的連結
要達成這件事情需要思考幾個面向:
為了解決第一個問題,我們先去頁面上看可能需要的資料有哪些
除了上方navbar中有一些連結之外,我們還可以看到sidebar中有那些註冊的app以及model
那我們要怎麼拿到這些資料?
此時我們先回去看admin中的模板
首先是base.html
進到nav_sidebar.html
最後看到app_list.html
我們可以看到我們需要app_list的資料
此時我們可以選擇生出app_list,但是除了要自己生資料出來之外,還必須要處理上面許多的模板標籤。因此我們可以透過重新製作一個Admin的後台,並且繼承基礎的資料來做
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
:
admin_site
中的上下文,先不管admin_site要怎麼拿,但是這裡的admin_site也就是admin.AdminSite
對象,相當於是Django預設的後台對象admin.AdminSite
上拿到基礎的app_list
app_list
中,這樣最後sidebar才能出現連結最後重寫as_view方法,因為是類方法,所以能確保admin_site能夠被在視圖被創建時就附在上面
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
get_urls
中,我們要將客製化的路由還有視圖加入self.admin_view(CustomView.as_view(admin_site=self))
中有幾個重要的訊息
admin_view
的源碼如下,他可以確保用戶登入,權限檢查等等的問題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
如果覺得上面說明得不夠清楚,我有請claude幫忙生出sequenceDiagram
希望能讓讀者更清楚整個流程
今天我們又更深入的探討要怎麼客製化我們的後台網站,尤其是最後的部分
讓我們對於後台的掌握度又level up了!但是同時我們也可以發現,Django雖然封裝好了相當多的功能,但是要進行改寫或是debug時,反而會因為過度封裝而需要更了解內部運作的原理造成難度上升
明天我們還是會針對後台做文章,我們來看一下跟後台相關的一些第三方庫!