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
相关产品推荐
相关产品推荐

