欢迎光临
我们一直在努力

〔九〕Admin内联与用户资料一体化编辑

📢 阅读提示

感谢你阅读本章。本文所有代码和注释均来自真实项目,为了能让更多认真学习的读者持续获得优质内容,本文将在7天后转为“粉丝可见”。 如果你觉得本文有价值,欢迎收藏并关注我,本系列博客将继续保持同样详细的注释风格。 感谢理解与支持!


一、业务场景

在第7章我们创建了UserProfile模型,通过OneToOneField扩展了Django自带的User。但在默认的Admin中,User和UserProfile是分开管理的:

· 用户管理页面只能修改username、password、email等基础字段。 · 要修改用户的部门、职位、业务角色,必须单独打开UserProfile的管理页面。

这种分离操作效率低下,且容易遗漏。管理员希望能在一个页面上同时编辑用户的基础信息和扩展资料。

解决方案:使用Django Admin的内联(Inline)功能,将UserProfile作为内联表单嵌入到User的编辑页面中。同时,为了提升用户体验,部门选择框应该只显示当前管理员有权查看的部门,并且以下拉树状结构呈现(带缩进,一眼看出层级关系)。

二、业务要求

  • 在User的编辑页面中,下方直接显示UserProfile的编辑区域(内联)。
  • 内联表单中,部门选择框只能看到当前登录用户(管理员)有权查看的部门(通过get_viewable_departments过滤)。
  • 部门选择框以树状缩进形式显示(例如“├─ 治安科”),方便管理员选择正确的上级部门。
  • 内联表单中不能删除UserProfile(只能通过软删除标记),因此设置can_delete=False。
  • 当用户新建时,created_by和updated_by等审计字段应在后台自动填充(但本章只展示内联,审计填充已在第6章介绍或在save_model中处理)。
  • 三、代码实现(逐行注释)

    以下代码来自你实际的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异常。

    五、注意事项(踩坑经验)

  • can_delete=False 的必要性: 如果不设置,内联表单下方会出现“删除”复选框,用户可能会误删UserProfile。由于我们使用软删除机制,删除操作应该通过soft_delete方法,而不是物理删除。
  • fk_name 的指定:当模型中有多个外键指向同一个关联模型时,需要明确指定fk_name。这里UserProfile只有一个外键user,不指定也可以,但显式指定更清晰。
  • 树状下拉框的排序:仅仅依靠DepartmentChoiceField.label_from_instance设置显示文本还不够,因为下拉框的选项顺序默认是按照queryset的默认顺序(可能是id)。我们需要使用get_department_tree_choices生成的选项列表来设置field.choices,确保选项按照树状顺序排列。
  • 权限过滤的完整性:formfield_for_foreignkey中的部门选择范围已经限制为get_viewable_departments,但还要注意,当用户编辑一个已有记录时,如果该记录的部门已经不在权限范围内(例如用户被调离),我们仍然要显示它。我们通过final_queryset = viewable_departments | Department.objects.filter(id=current_dept.id)实现了这一点。
  • get_formset 中的 self._current_obj 生命周期:self在整个请求处理期间有效,但要注意如果同一个页面中使用了多个内联,可能会相互干扰。目前只有一个内联,安全。
  • 表单 UserProfileBusinessForm 中的多选框处理:因为数据库存储的是逗号分隔字符串,而表单使用MultipleChoiceField,所以需要在__init__中将字符串转为列表,在save中将列表转回字符串。
  • 六、小结

    本章我们完成了:

    · 创建UserProfileInline,将用户资料嵌入到User编辑页。 · 重写formfield_for_foreignkey,实现部门下拉框的权限过滤和树状显示。 · 自定义CustomUserAdmin,挂载内联并增加列表页的扩展显示。 · 简要说明了UserProfileBusinessForm如何处理业务角色多选框。

    至此,管理员可以在一个页面中轻松维护用户及其所有扩展信息,大大提升了后台管理效率。

    下一章我们将深入 Admin列表页的权限过滤,利用get_viewable_departments实现数据隔离,并动态控制批量操作和字段显示。


    互动问题:你在Django Admin中使用内联时,遇到过哪些“坑”?比如外键字段过多导致页面卡顿?或者内联表单保存时数据丢失?欢迎评论区交流。


    赞(0)
    未经允许不得转载:171主机测评 » 〔九〕Admin内联与用户资料一体化编辑
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址