跳到主要内容

Admin 后台

Django Admin 是 Django 最强大的功能之一,它可以根据模型自动生成功能完备的管理后台,让你可以快速管理网站数据。

基本配置

创建管理员账户

python manage.py createsuperuser

按提示输入用户名、邮箱和密码。

访问管理后台

启动开发服务器后,访问 /admin/ 即可进入管理后台。

注册模型

# admin.py
from django.contrib import admin
from .models import Post, Category, Tag


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
pass


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
pass


# 或使用简单注册
admin.site.register(Tag)

ModelAdmin 选项

列表显示

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# 列表显示字段
list_display = ['title', 'author', 'category', 'status', 'publish', 'views']

# 列表可点击排序字段
list_display_links = ['title']

# 列表可编辑字段
list_editable = ['status']

# 列表过滤
list_filter = ['status', 'category', 'author', 'publish']

# 搜索字段
search_fields = ['title', 'content', 'author__username']

# 日期层级导航
date_hierarchy = 'publish'

# 排序
ordering = ['-publish']

# 每页显示数量
list_per_page = 20

# 显示全部链接的最大数量
list_max_show_all = 100

# 外键自动完成
autocomplete_fields = ['author', 'category']

# 多对多水平显示
filter_horizontal = ['tags']

# 多对多垂直显示
filter_vertical = ['tags']

# 原始 ID 输入(适合大量数据的外键)
raw_id_fields = ['author']

编辑表单

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# 表单字段
fields = ['title', 'slug', 'content', 'status', 'category', 'tags']

# 字段分组
fieldsets = (
('基本信息', {
'fields': ('title', 'slug', 'content')
}),
('分类和标签', {
'fields': ('category', 'tags'),
'classes': ('collapse',), # 可折叠
}),
('发布设置', {
'fields': ('status', 'author'),
'description': '设置文章的发布状态', # 描述文本
}),
)

# 排除字段
exclude = ['views']

# 只读字段
readonly_fields = ['views', 'publish', 'created', 'updated']

# 保存按钮在顶部
save_on_top = True

# 保存为新的
save_as = True

# 保存为新的继续编辑
save_as_continue = True

# 预填充字段
prepopulated_fields = {'slug': ('title',)}

自定义列表显示

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'status', 'publish', 'view_count']

def view_count(self, obj):
"""自定义列显示"""
return f'{obj.views:,} 次阅读'
view_count.short_description = '阅读量'
view_count.admin_order_field = 'views' # 可排序

def get_queryset(self, request):
"""自定义查询集"""
queryset = super().get_queryset(request)
return queryset.select_related('author', 'category')

def status_colored(self, obj):
"""带颜色的状态显示"""
from django.utils.html import format_html
colors = {
'draft': 'gray',
'published': 'green',
}
return format_html(
'<span style="color: {};">{}</span>',
colors.get(obj.status, 'black'),
obj.get_status_display()
)
status_colored.short_description = '状态'

内联编辑

内联编辑允许在父模型的编辑页面中编辑相关模型:

TabularInline

表格形式:

class CommentInline(admin.TabularInline):
model = Comment
extra = 1 # 额外空白行数
readonly_fields = ['author', 'content', 'created']
show_change_link = True # 显示编辑链接


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]

StackedInline

堆叠形式:

class CommentInline(admin.StackedInline):
model = Comment
extra = 0
fields = ['author', 'content', 'active']
verbose_name = '评论'
verbose_name_plural = '评论列表'

def has_add_permission(self, request, obj=None):
"""禁止添加"""
return False

def has_delete_permission(self, request, obj=None):
"""禁止删除"""
return False


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline]

自定义操作

批量操作

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
actions = ['make_published', 'make_draft', 'export_as_csv']

def make_published(self, request, queryset):
"""批量发布"""
count = queryset.update(status='published')
self.message_user(request, f'成功发布 {count} 篇文章')
make_published.short_description = '发布所选文章'

def make_draft(self, request, queryset):
"""批量设为草稿"""
count = queryset.update(status='draft')
self.message_user(request, f'成功将 {count} 篇文章设为草稿')
make_draft.short_description = '设为草稿'

def export_as_csv(self, request, queryset):
"""导出为 CSV"""
import csv
from django.http import HttpResponse

response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="posts.csv"'

writer = csv.writer(response)
writer.writerow(['标题', '作者', '状态', '发布时间'])

for post in queryset:
writer.writerow([
post.title,
post.author.username,
post.get_status_display(),
post.publish.strftime('%Y-%m-%d %H:%M'),
])

return response
export_as_csv.short_description = '导出为 CSV'

自定义操作页面

from django.contrib import admin
from django.shortcuts import render
from django.http import HttpResponseRedirect


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
actions = ['change_status']

def change_status(self, request, queryset):
"""自定义操作页面"""
if 'apply' in request.POST:
status = request.POST.get('status')
count = queryset.update(status=status)
self.message_user(request, f'成功更新 {count} 篇文章状态')
return HttpResponseRedirect(request.get_full_path())

return render(request, 'admin/change_status.html', {
'posts': queryset,
'status_choices': Post.STATUS_CHOICES,
})
change_status.short_description = '更改文章状态'

自定义管理界面

自定义管理模板

templates/admin/ 目录下创建自定义模板:

templates/
└── admin/
├── base_site.html # 覆盖基础模板
├── index.html # 首页
├── change_list.html # 列表页
└── change_form.html # 编辑页

自定义站点标题:

<!-- templates/admin/base_site.html -->
{% extends "admin/base_site.html" %}

{% block title %}{{ title }} | 我的网站管理{% endblock %}

{% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">我的网站管理后台</a>
</h1>
{% endblock %}

自定义 CSS 和 JS

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
class Media:
css = {
'all': ('css/admin/custom.css',)
}
js = ('js/admin/custom.js',)

自定义站点

# admin.py
from django.contrib import admin


class MyAdminSite(admin.AdminSite):
site_header = '我的网站管理'
site_title = '网站管理'
index_title = '欢迎来到管理后台'
site_url = '/blog/'


my_admin_site = MyAdminSite(name='myadmin')


# urls.py
from django.urls import path
from myapp.admin import my_admin_site

urlpatterns = [
path('myadmin/', my_admin_site.urls),
]

权限控制

基于模型的权限

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
# 查看权限
def has_view_permission(self, request, obj=None):
return True

# 添加权限
def has_add_permission(self, request):
return request.user.is_staff

# 修改权限
def has_change_permission(self, request, obj=None):
if obj is None:
return True
return obj.author == request.user or request.user.is_superuser

# 删除权限
def has_delete_permission(self, request, obj=None):
if obj is None:
return True
return obj.author == request.user or request.user.is_superuser

# 模块权限
def has_module_permission(self, request):
return request.user.is_staff

限制数据访问

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
def get_queryset(self, request):
"""只显示用户自己的文章"""
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)

def save_model(self, request, obj, form, change):
"""自动设置作者"""
if not change:
obj.author = request.user
super().save_model(request, obj, form, change)

def get_form(self, request, obj=None, **kwargs):
"""根据用户权限修改表单"""
form = super().get_form(request, obj, **kwargs)
if not request.user.is_superuser:
form.base_fields['author'].disabled = True
return form

第三方扩展

django-ckeditor

富文本编辑器:

pip install django-ckeditor
# settings.py
INSTALLED_APPS = [
# ...
'ckeditor',
]

# models.py
from ckeditor.fields import RichTextField

class Post(models.Model):
content = RichTextField()

# admin.py
from ckeditor.widgets import CKEditorWidget

class PostAdmin(admin.ModelAdmin):
formfield_overrides = {
models.TextField: {'widget': CKEditorWidget},
}

django-import-export

数据导入导出:

pip install django-import-export
# admin.py
from import_export.admin import ImportExportModelAdmin
from import_export import resources


class PostResource(resources.ModelResource):
class Meta:
model = Post
fields = ['id', 'title', 'author__username', 'status', 'publish']


@admin.register(Post)
class PostAdmin(ImportExportModelAdmin):
resource_class = PostResource
list_display = ['title', 'author', 'status', 'publish']

django-simple-history

记录数据变更历史:

pip install django-simple-history
# models.py
from simple_history.models import HistoricalRecords

class Post(models.Model):
title = models.CharField(max_length=200)
history = HistoricalRecords()

# admin.py
from simple_history.admin import SimpleHistoryAdmin

@admin.register(Post)
class PostAdmin(SimpleHistoryAdmin):
list_display = ['title', 'status']
history_list_display = ['status']

完整示例

# admin.py
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from .models import Post, Category, Tag, Comment


class CommentInline(admin.TabularInline):
model = Comment
extra = 0
readonly_fields = ['author', 'content', 'created']
show_change_link = True


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug', 'post_count']
prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']

def post_count(self, obj):
return obj.posts.count()
post_count.short_description = '文章数量'


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = [
'title', 'author_link', 'category_link',
'status_badge', 'publish', 'view_count'
]
list_filter = ['status', 'category', 'author', 'publish']
search_fields = ['title', 'content', 'author__username']
date_hierarchy = 'publish'
ordering = ['-publish']
list_per_page = 20

fieldsets = (
('文章内容', {
'fields': ('title', 'slug', 'content', 'featured_image')
}),
('分类与标签', {
'fields': ('category', 'tags'),
'classes': ('collapse',),
}),
('发布设置', {
'fields': ('status', 'author'),
}),
)

readonly_fields = ['views', 'publish', 'created', 'updated']
prepopulated_fields = {'slug': ('title',)}
filter_horizontal = ['tags']
inlines = [CommentInline]

actions = ['make_published', 'make_draft']

def author_link(self, obj):
url = reverse('admin:auth_user_change', args=[obj.author.pk])
return format_html('<a href="{}">{}</a>', url, obj.author.username)
author_link.short_description = '作者'
author_link.admin_order_field = 'author__username'

def category_link(self, obj):
if obj.category:
url = reverse('admin:blog_category_change', args=[obj.category.pk])
return format_html('<a href="{}">{}</a>', url, obj.category.name)
return '-'
category_link.short_description = '分类'

def status_badge(self, obj):
colors = {'draft': 'secondary', 'published': 'success'}
return format_html(
'<span class="badge bg-{}">{}</span>',
colors.get(obj.status, 'secondary'),
obj.get_status_display()
)
status_badge.short_description = '状态'
status_badge.admin_order_field = 'status'

def view_count(self, obj):
return format_html('{} 次', obj.views)
view_count.short_description = '阅读量'
view_count.admin_order_field = 'views'

def make_published(self, request, queryset):
count = queryset.update(status='published')
self.message_user(request, f'成功发布 {count} 篇文章')
make_published.short_description = '发布所选文章'

def make_draft(self, request, queryset):
count = queryset.update(status='draft')
self.message_user(request, f'成功将 {count} 篇文章设为草稿')
make_draft.short_description = '设为草稿'

def get_queryset(self, request):
return super().get_queryset(request).select_related('author', 'category')

def save_model(self, request, obj, form, change):
if not change:
obj.author = request.user
super().save_model(request, obj, form, change)


@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['author', 'post_link', 'content_preview', 'active', 'created']
list_filter = ['active', 'created']
search_fields = ['author__username', 'content', 'post__title']
actions = ['approve_comments', 'disapprove_comments']

def post_link(self, obj):
url = reverse('admin:blog_post_change', args=[obj.post.pk])
return format_html('<a href="{}">{}</a>', url, obj.post.title)
post_link.short_description = '文章'

def content_preview(self, obj):
return obj.content[:50] + '...' if len(obj.content) > 50 else obj.content
content_preview.short_description = '内容'

def approve_comments(self, request, queryset):
count = queryset.update(active=True)
self.message_user(request, f'成功批准 {count} 条评论')
approve_comments.short_description = '批准所选评论'

def disapprove_comments(self, request, queryset):
count = queryset.update(active=False)
self.message_user(request, f'成功拒绝 {count} 条评论')
disapprove_comments.short_description = '拒绝所选评论'