You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django admin ModelForm下拉选择联动动态设置选项实现方法

Django Admin 新增表单下公司-员工筛选联动实现方案

Django admin中filter_horizontal组件的联动问题核心是新增场景无持久化实例,仅靠表单初始化时的queryset过滤只能覆盖编辑场景;直接用jQuery修改DOM选项无法持久,是因为filter_horizontal内置的SelectBox模块会缓存全量选项数据,DOM修改后只要触发搜索、选项移动等操作,组件就会用缓存的旧数据重绘,覆盖手动修改的内容。

完整实现分三步:

1. 后端新增员工查询接口

在对应的ModelAdmin中新增自定义视图,根据传入的公司ID返回关联员工列表,复用Django admin自带的鉴权逻辑,避免越权访问。

from django.http import JsonResponse
from django.contrib import admin
from django.urls import path
from .models import CompanyEmployee, Employee

class CompanyEmployeeAdmin(admin.ModelAdmin):
    filter_horizontal = ('employee',)
    list_filter = ('company',)
    form = CompanyEmployeeForm

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path(
                'get-employees-by-company/',
                self.admin_site.admin_view(self.get_employees_by_company),
                name='get-employees-by-company'
            ),
        ]
        return custom_urls + urls

    def get_employees_by_company(self, request):
        company_id = request.GET.get('company_id')
        # 基础权限校验
        if not request.user.has_perm('app.view_employee'):
            return JsonResponse({'error': 'permission denied'}, status=403)
        employees = []
        if company_id:
            employees = list(
                Employee.objects.filter(company_id=company_id)
                .values_list('id', 'name')
            )
        return JsonResponse({
            'employees': [{'id': e[0], 'name': e[1]} for e in employees]
        })

同时必须补充表单层的校验逻辑,避免前端篡改请求提交非法数据:

from django import forms

class CompanyEmployeeForm(forms.ModelForm):
    class Meta:
        model = CompanyEmployee
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.pk:
            self.fields['employee'].queryset = Employee.objects.filter(company=self.instance.company)
        else:
            # 新增场景初始不加载全量员工,减少无效数据传输
            self.fields['employee'].queryset = Employee.objects.none()

    def clean(self):
        cleaned_data = super().clean()
        selected_company = cleaned_data.get('company')
        selected_employees = cleaned_data.get('employee')
        if selected_company and selected_employees:
            invalid_employees = selected_employees.exclude(company=selected_company)
            if invalid_employees.exists():
                self.add_error('employee', '所选员工不属于当前绑定公司')
        return cleaned_data

2. 前端编写联动逻辑

在ModelAdmin中通过Media类引入自定义JS,监听公司下拉框的变更事件,拉取对应员工数据后同步更新filter_horizontal组件的缓存,解决选项不持久的问题。
首先在Admin类中添加静态资源配置:

class CompanyEmployeeAdmin(admin.ModelAdmin):
    # 省略上述已有配置
    class Media:
        js = ('admin/js/company_employee_linkage.js',)

然后在项目静态目录下创建admin/js/company_employee_linkage.js,写入以下逻辑:

(function($) {
    $(document).ready(function() {
        const $companySelect = $('#id_company');
        // filter_horizontal渲染后待选、已选框的固定ID
        const $waitSelect = $('#id_employee_from');
        const $selectedSelect = $('#id_employee_to');
        const $searchInput = $waitSelect.prevAll('.selector-filter input').first();

        // 刷新员工选项方法
        const loadEmployeeOptions = (companyId) => {
            if (!companyId) {
                $waitSelect.empty();
                $selectedSelect.empty();
                SelectBox.init('id_employee_from');
                SelectBox.init('id_employee_to');
                return;
            }
            $.getJSON(
                // 替换为自己的app名对应路径
                '/admin/your_app/companyemployee/get-employees-by-company/',
                {company_id: companyId},
                (res) => {
                    $waitSelect.empty();
                    $selectedSelect.empty();
                    res.employees.forEach(emp => {
                        $waitSelect.append(new Option(emp.name, emp.id, false, false));
                    });
                    // 重新初始化SelectBox缓存,避免选项被重绘覆盖
                    SelectBox.init('id_employee_from');
                    SelectBox.init('id_employee_to');
                    // 清空搜索框缓存内容
                    if ($searchInput.length) {
                        $searchInput.val('').trigger('keyup');
                    }
                }
            )
        }

        // 初始加载时如果公司有默认值,自动加载对应员工
        if ($companySelect.val()) {
            loadEmployeeOptions($companySelect.val());
        }

        // 监听公司下拉变更
        $companySelect.on('change', function() {
            loadEmployeeOptions($(this).val());
        })
    })
})(django.jQuery);

注意将代码中接口路径的your_app替换为你项目中模型所在的app名称,否则接口会返回404。


实现要点

  • 不要直接修改filter_horizontal的DOM选项,必须调用SelectBox.init()方法更新组件内部缓存,否则修改的内容会被组件重绘覆盖
  • 后端校验不可省略,前端的选项限制仅为交互优化,无法拦截构造的恶意请求
  • 接口必须用admin_site.admin_view包装,复用admin的登录、权限校验逻辑,避免未授权访问

内容的提问来源于stack exchange,提问作者Suvodeep Dubey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 15:51:43