📢 阅读提示
感谢你阅读本章。本文所有代码和注释均来自真实项目,为了能让更多认真学习的读者持续获得优质内容,本文将在7天后转为“粉丝可见”。 如果你觉得本文有价值,欢迎收藏并关注我,本系列博客将继续保持同样详细的注释风格。 感谢理解与支持!
一、业务场景
在第7章我们创建了UserProfile模型,通过OneToOneField扩展了Django自带的User。但在默认的Admin中,User和UserProfile是分开管理的:
· 用户管理页面只能修改username、password、email等基础字段。 · 要修改用户的部门、职位、业务角色,必须单独打开UserProfile的管理页面。
这种分离操作效率低下,且容易遗漏。管理员希望能在一个页面上同时编辑用户的基础信息和扩展资料。
解决方案:使用Django Admin的内联(Inline)功能,将UserProfile作为内联表单嵌入到User的编辑页面中。同时,为了提升用户体验,部门选择框应该只显示当前管理员有权查看的部门,并且以下拉树状结构呈现(带缩进,一眼看出层级关系)。
二、业务要求
三、代码实现(逐行注释)
以下代码来自你实际的organization/admin.py中的UserProfileInline和CustomUserAdmin。我们将其拆解并详细解释。
3.1 内联类:UserProfileInline
# organization/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django import forms
from common.department import get_viewable_departments, DepartmentChoiceField, get_department_tree_choices
from organization.models import UserProfile
from organization.forms import UserProfileBusinessForm # 这个表单包含了业务角色字段的定制
class UserProfileInline(admin.StackedInline):
"""
将UserProfile作为内联,嵌入到User的编辑页面中。
使用StackedInline(垂直堆叠布局)而不是TabularInline(表格布局),
因为UserProfile字段较多,垂直布局更清晰。
"""
model = UserProfile # 关联的模型
form = UserProfileBusinessForm # 自定义表单(处理业务角色多选框等)
can_delete = False # 禁止从内联中删除UserProfile(应该通过软删除)
verbose_name_plural = '用户资料' # 内联区域的标题
fk_name = 'user' # 外键字段名(UserProfile.user)
# 内联表单中显示的字段(顺序从上到下)
fields = (
'department', # 所属部门(外键下拉框)
'position', # 职位
'phone', # 移动电话
'office_phone', # 办公电话
'room_number', # 房间号
'employee_id', # 员工编号
'business_role', # 业务角色
'business_categories', # 负责业务(多选)
'is_department_admin', # 是否单位管理员
'notes', # 备注
'is_deleted' # 软删除标记(只读)
)
# 只读字段:这些字段在页面中显示但不可编辑
readonly_fields = ('created_at', 'updated_at', 'deleted_at', 'deleted_by', 'delete_reason')
def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""
重写外键字段的表单控件,用于限制部门选择范围并实现树状下拉框。
当字段是 'department' 时,使用自定义的 DepartmentChoiceField。
"""
if db_field.name == "department":
# 1. 获取当前登录用户有权查看的部门
viewable_departments = get_viewable_departments(request.user)
# 2. 处理编辑已有记录的情况:如果当前用户所属部门不在权限范围内,仍然要显示(否则用户会看到空白)
# 我们保留这个逻辑,但通过后续的 choices 已经包含了所有部门,所以这里只需确保 queryset 包含当前部门。
final_queryset = viewable_departments
if hasattr(self, '_current_obj') and self._current_obj:
current_dept = self._current_obj.profile.department
if current_dept and current_dept not in viewable_departments:
# 使用 OR 合并查询集,确保当前部门也在列表中
from django.db.models import Q
final_queryset = viewable_departments | Department.objects.filter(id=current_dept.id)
# 3. 使用自定义的 DepartmentChoiceField(它会自动调用 label_from_instance 生成树状显示)
field = DepartmentChoiceField(
queryset=final_queryset,
required=False,
label="所属单位",
empty_label="———",
widget=forms.Select(attrs={'style': 'width: 300px;'})
)
# 4. 生成树状顺序的选项(虽然 DepartmentChoiceField 的 label_from_instance 已经处理了缩进,
# 但为了确保下拉框中的顺序是树状的,我们仍然设置 choices)
# 注意:get_department_tree_choices 返回的选项已经按树状顺序排列并带缩进。
tree_choices = get_department_tree_choices(final_queryset, include_empty=True)
field.choices = tree_choices
return field
# 其他外键字段使用默认处理
return super().formfield_for_foreignkey(db_field, request, **kwargs)
def get_formset(self, request, obj=None, **kwargs):
"""
存储当前正在编辑的 User 对象,供 formfield_for_foreignkey 使用。
因为 formfield_for_foreignkey 无法直接访问到当前编辑的父对象(User),
我们需要通过 get_formset 把它存到 self._current_obj 中。
"""
self._current_obj = obj # obj 就是当前的 User 实例
return super().get_formset(request, obj, **kwargs)
3.2 自定义UserAdmin:将内联挂载到User编辑页
# 首先取消Django默认的UserAdmin注册,再重新注册
admin.site.unregister(User)
@admin.register(User)
class CustomUserAdmin(UserAdmin):
"""
自定义User管理类,添加内联(UserProfileInline),
并调整列表显示字段、过滤器、搜索字段等。
"""
# 挂载内联
inlines = [UserProfileInline]
# 列表页显示的字段(添加了一些扩展信息的自定义方法)
list_display = (
'username',
'is_active',
'is_staff',
'get_department', # 自定义方法,显示部门名称
'get_business_role_display', # 自定义方法,显示业务角色
'get_business_categories_display', # 自定义方法,显示负责业务
'get_deleted_status' # 自定义方法,显示软删除状态
)
# 列表页过滤器
list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups', 'date_joined',
'profile__is_deleted', 'profile__business_role')
# 搜索字段
search_fields = ('username', 'email', 'first_name', 'last_name')
# 自定义显示方法
def get_department(self, obj):
try:
return obj.profile.department.name if obj.profile.department else "无部门"
except UserProfile.DoesNotExist:
return "无部门"
get_department.short_description = "所属部门"
def get_business_role_display(self, obj):
try:
return obj.profile.get_business_role_display()
except UserProfile.DoesNotExist:
return "无"
get_business_role_display.short_description = "业务角色"
def get_business_categories_display(self, obj):
try:
return obj.profile.get_business_categories_display()
except UserProfile.DoesNotExist:
return "无"
get_business_categories_display.short_description = "负责业务"
def get_deleted_status(self, obj):
try:
if obj.profile.is_deleted:
from django.utils.html import format_html
return format_html('<span style="color:red;">已删除</span>')
except UserProfile.DoesNotExist:
pass
return "正常"
get_deleted_status.short_description = "状态"
# 其他定制(如批量操作、权限过滤等)将在后续章节详述,这里只保留内联相关设置。
3.3 自定义表单:UserProfileBusinessForm(简要说明)
在organization/forms.py中,我们定义了UserProfileBusinessForm,它主要处理业务角色(business_role)的下拉框和负责业务(business_categories)的多选框。
# organization/forms.py(概要)
from django import forms
from organization.models import UserProfile
from organization.models import BUSINESS_ROLE_CHOICES, BUSINESS_CATEGORY_CHOICES
class UserProfileBusinessForm(forms.ModelForm):
"""
用户资料表单,用于内联和独立编辑。
主要定制:将business_categories字段显示为多选框(CheckboxSelectMultiple)。
"""
business_categories = forms.MultipleChoiceField(
choices=BUSINESS_CATEGORY_CHOICES,
required=False,
label="负责业务",
widget=forms.CheckboxSelectMultiple
)
class Meta:
model = UserProfile
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 将数据库中存储的逗号分隔字符串转换成列表,用于多选框初始值
if self.instance and self.instance.business_categories:
categories_list = self.instance.get_business_categories_list()
self.initial['business_categories'] = categories_list
def save(self, commit=True):
# 将多选框选中的列表转换回逗号分隔字符串再保存
instance = super().save(commit=False)
categories = self.cleaned_data.get('business_categories', [])
instance.set_business_categories_list(categories)
if commit:
instance.save()
return instance
四、核心方法详解
1. StackedInline 与 TabularInline 的选择
· StackedInline:每个关联对象占一个垂直区域,字段堆叠显示,适合字段较多的情况。 · TabularInline:以表格行形式显示,适合字段较少且需要紧凑展示的场景。
UserProfile字段较多,所以我们选用StackedInline。
2. formfield_for_foreignkey 重写
此方法允许我们对外键字段(department)的表单控件进行完全定制。步骤:
· 获取当前用户有权查看的部门(get_viewable_departments)。 · 处理编辑已有记录时,当前部门可能不在权限范围内的问题(为了能够显示原有数据,将其添加到查询集中)。 · 使用自定义的DepartmentChoiceField(它重写了label_from_instance,根据部门层级生成带缩进的文本)。 · 调用get_department_tree_choices生成树状顺序的选项列表,确保下拉框中的顺序也是树状的。
3. get_formset 存储当前对象
在formfield_for_foreignkey中我们无法直接获取正在编辑的User对象(obj),但我们可以通过重写get_formset,将obj暂存在self._current_obj中,供formfield_for_foreignkey使用。这是一个常用技巧。
4. 自定义UserAdmin
通过admin.site.unregister(User)先取消默认注册,再用@admin.register(User)注册自己的CustomUserAdmin,并指定inlines = [UserProfileInline]。这样在User编辑页就会自动显示内联区域。
5. 自定义显示方法
在list_display中,我们添加了get_department、get_business_role_display等方法,这些方法通过obj.profile访问关联的UserProfile,并返回可读的内容。注意要用try…except处理UserProfile.DoesNotExist异常。
五、注意事项(踩坑经验)
六、小结
本章我们完成了:
· 创建UserProfileInline,将用户资料嵌入到User编辑页。 · 重写formfield_for_foreignkey,实现部门下拉框的权限过滤和树状显示。 · 自定义CustomUserAdmin,挂载内联并增加列表页的扩展显示。 · 简要说明了UserProfileBusinessForm如何处理业务角色多选框。
至此,管理员可以在一个页面中轻松维护用户及其所有扩展信息,大大提升了后台管理效率。
下一章我们将深入 Admin列表页的权限过滤,利用get_viewable_departments实现数据隔离,并动态控制批量操作和字段显示。
互动问题:你在Django Admin中使用内联时,遇到过哪些“坑”?比如外键字段过多导致页面卡顿?或者内联表单保存时数据丢失?欢迎评论区交流。



