Django 4.1.3:如何在ModelForm中用选中外键过滤另一外键查询集
问题场景
用Django 4.1.3开发时,需要在EntryForm这个ModelForm里,根据用户选中的exhibitor外键ID,动态过滤pen外键的可选选项。目前代码里用固定ID=1能实现过滤,但拿不到用户选中的exhibitor值;试了self.fields['pen']当过滤条件,结果在/admin/exhibitors/entry/add/页面触发TypeError:字段'id'需要数字,但得到了<django.forms.models.ModelChoiceField object at 0x2b630a6b2100>。另外还想知道能不能用pre_save函数实现这个需求。
现有代码:
from django import forms from .models import Entry, Exhibitor, Pen class EntryForm(forms.ModelForm): class Meta: fields = ('exhibitor', 'pen', 'class_id',) def __init__(self, *args, **kwargs): super(EntryForm, self).__init__(*args, **kwargs) # 不知道怎么用选中的exhibitor值过滤 self.fields['pen'].queryset = Pen.objects.filter(current_owner_id=1) # 这里的1应该换成选中的exhibitor_id # 用固定整数能过滤,但不知道怎么获取选中字段的值,或者能不能在pre_save里实现?
试过的错误代码:
self.fields['pen'].queryset = Pen.objects.filter(current_owner_id=self.fields['pen'])
解决方案
1. 编辑页面的静态过滤(初始化时获取已有值)
如果是编辑已有的Entry记录,能在__init__方法里从实例中拿到已关联的exhibitorID来过滤:
def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 编辑已有对象时,用关联的exhibitor过滤pen选项 if self.instance and self.instance.exhibitor: self.fields['pen'].queryset = Pen.objects.filter(current_owner_id=self.instance.exhibitor.id) else: # 新增页面默认不显示任何pen选项,等用户选完exhibitor再加载 self.fields['pen'].queryset = Pen.objects.none()
2. 新增页面的动态过滤(需要前端JS配合)
上面的方法只适用于编辑页面,新增页面因为用户还没选exhibitor,得用JavaScript监听下拉框变化,异步请求获取对应的pen选项再更新下拉框。
在Django Admin里实现的话,先自定义Admin类:
from django.contrib import admin from .models import Entry from .forms import EntryForm class EntryAdmin(admin.ModelAdmin): form = EntryForm class Media: js = ('admin/js/entry_pen_filter.js',)
然后创建static/admin/js/entry_pen_filter.js文件:
document.addEventListener('DOMContentLoaded', function() { const exhibitorSelect = document.getElementById('id_exhibitor'); const penSelect = document.getElementById('id_pen'); // 初始状态下禁用pen下拉框,避免用户乱选 penSelect.disabled = true; exhibitorSelect.addEventListener('change', function() { const exhibitorId = this.value; if (exhibitorId) { // 发AJAX请求获取对应exhibitor的pen列表 fetch(`/admin/exhibitors/pen/filter/?exhibitor_id=${exhibitorId}`) .then(response => response.json()) .then(data => { // 清空现有选项 penSelect.innerHTML = '<option value="">---------</option>'; // 循环添加新的pen选项 data.forEach(pen => { const option = document.createElement('option'); option.value = pen.id; option.textContent = pen.name; // 这里假设Pen模型有name字段,根据实际调整 penSelect.appendChild(option); }); penSelect.disabled = false; }) .catch(error => console.error('加载pen选项出错:', error)); } else { // 没选exhibitor时,清空pen选项并禁用 penSelect.innerHTML = '<option value="">---------</option>'; penSelect.disabled = true; } }); });
接下来要加一个视图处理AJAX请求:
# 在app的views.py里 from django.http import JsonResponse from .models import Pen def filter_pens_by_exhibitor(request): exhibitor_id = request.GET.get('exhibitor_id') # 取出对应exhibitor的pen,只返回id和显示用的字段(这里用name) pens = Pen.objects.filter(current_owner_id=exhibitor_id).values('id', 'name') return JsonResponse(list(pens), safe=False)
最后在项目的urls.py里加路由:
from django.urls import path from . import views urlpatterns = [ # 其他已有的路由... path('admin/exhibitors/pen/filter/', views.filter_pens_by_exhibitor, name='filter_pens_by_exhibitor'), ]
3. 关于pre_save的疑问
pre_save信号或者ModelAdmin的save_model方法,只能在保存数据前做验证或修改,没法用来动态过滤表单的可选选项——因为过滤是要在用户填写表单时就生效的,而pre_save是用户提交表单之后才触发的,所以完全不适合做这个需求。
内容的提问来源于stack exchange,提问作者beepbop

