如何在Django Admin中限制外键选项,处理InlineForm依赖主表单问题
1. 如何在Django的Admin表单集中限制外键选项仅为关联对象
这里分两种场景处理,看你使用的是普通批量编辑表单集还是Inline表单集:
场景A:普通批量编辑表单集
假设你有Project和Task两个模型,Task关联Project,现在要批量编辑属于某个特定Project的Task,希望每个Task的project外键只能选这个指定的Project。
先自定义表单,在初始化时过滤外键的查询集:
from django import forms from .models import Task, Project class RestrictedTaskForm(forms.ModelForm): class Meta: model = Task fields = ['project', 'name'] def __init__(self, *args, **kwargs): # 从参数里拿到指定的project_id self.target_project_id = kwargs.pop('project_id', None) super().__init__(*args, **kwargs) if self.target_project_id: # 把project选项限制为指定的那个Project self.fields['project'].queryset = Project.objects.filter(id=self.target_project_id) # 如果是已存在的Task,直接把project设为目标值并禁用编辑 if self.instance.pk: self.fields['project'].initial = self.target_project_id self.fields['project'].widget.attrs['disabled'] = True
然后在Admin里配置表单集,传递project_id参数:
from django.contrib import admin from django.forms import formset_factory from .models import Task class TaskAdmin(admin.ModelAdmin): def get_changelist_formset(self, request, **kwargs): # 这里假设从URL参数里获取project_id,你也可以根据业务逻辑从其他地方拿 target_project_id = request.GET.get('project_id') # 创建表单集,给每个表单传入project_id TaskFormSet = formset_factory(RestrictedTaskForm, extra=1) kwargs['form_kwargs'] = {'project_id': target_project_id} return super().get_changelist_formset(request, **kwargs)
场景B:Inline表单集(父对象编辑页面)
如果是在Project的Admin页面里用Inline展示Task,要限制Task的project外键只能是当前编辑的Project,可以直接在Inline里过滤:
from django.contrib import admin from .models import Task class TaskInline(admin.TabularInline): model = Task form = RestrictedTaskForm def get_formset(self, request, obj=None, **kwargs): formset = super().get_formset(request, obj, **kwargs) # obj就是当前正在编辑的Project对象,直接过滤queryset formset.form.base_fields['project'].queryset = Project.objects.filter(id=obj.id) return formset
2. 如何在Django Admin中根据AdminForm选中的对象,限制InlineForm的ForeignKeyField选项?
这个问题的痛点是:当编辑父对象时,Inline表单默认拿不到父对象的信息(尤其是新建父对象还没保存的时候),分两种情况解决:
情况一:编辑已存在的父对象
如果父对象已经保存在数据库里,我们可以直接通过obj参数拿到父对象,然后过滤Inline表单的外键。
举个例子:有Client→Project→Task三层关联,Task有一个Milestone外键,而Milestone属于Client。现在在Project的Admin页面编辑时,希望Task的Milestone选项只能是当前Project所属Client的Milestone。
先自定义Task的表单:
from django import forms from .models import Task, Milestone class TaskWithRestrictedMilestoneForm(forms.ModelForm): class Meta: model = Task fields = ['milestone', 'name'] def __init__(self, *args, **kwargs): # 拿到当前的Project对象 self.current_project = kwargs.pop('project', None) super().__init__(*args, **kwargs) if self.current_project: # 过滤Milestone仅为当前Project所属Client的记录 self.fields['milestone'].queryset = Milestone.objects.filter(client=self.current_project.client)
然后在Inline里传递父对象(Project)给表单:
from django.contrib import admin from .models import Task class TaskInline(admin.TabularInline): model = Task form = TaskWithRestrictedMilestoneForm def get_formset(self, request, obj=None, **kwargs): formset = super().get_formset(request, obj, **kwargs) # obj就是当前编辑的Project对象,传给每个表单 formset.form_kwargs = {'project': obj} return formset
这样打开已有的Project编辑页面时,Task的Milestone选项就会自动过滤了。
情况二:新建父对象(未保存)
如果是新建父对象,此时父对象还没有ID,后端拿不到关联信息,这时候就得靠前端JS配合AJAX来动态更新选项。
还是用上面的场景:新建Project时,选中Client后,Task的Milestone选项自动更新为该Client下的记录。
步骤1:给ProjectAdmin添加JS文件
from django.contrib import admin from .models import Project class ProjectAdmin(admin.ModelAdmin): inlines = [TaskInline] class Media: # 引入自定义JS,路径根据你的项目结构调整 js = ('admin/js/restrict_milestone.js',)
步骤2:编写JS文件(restrict_milestone.js)
document.addEventListener('DOMContentLoaded', function() { // 拿到父表单里的Client选择框 const clientSelect = document.getElementById('id_client'); // 拿到所有Inline里的Milestone选择框 const milestoneSelects = document.querySelectorAll('.inline-group[data-inline-type="tabular"] select[name$="-milestone"]'); // 更新Milestone选项的函数 function updateMilestoneOptions(clientId) { if (!clientId) { // 没选Client的话,清空选项 milestoneSelects.forEach(select => { select.innerHTML = '<option value="">---------</option>'; }); return; } // 发送AJAX请求获取该Client的Milestone列表 fetch(`/admin/your_app_name/milestone/get_by_client/?client_id=${clientId}`) .then(response => response.json()) .then(data => { milestoneSelects.forEach(select => { let options = '<option value="">---------</option>'; data.forEach(milestone => { options += `<option value="${milestone.id}">${milestone.name}</option>`; }); select.innerHTML = options; }); }); } // 页面加载时,如果是编辑已有Project,先初始化一次 if (clientSelect.value) { updateMilestoneOptions(clientSelect.value); } // 监听Client选择框的变化 clientSelect.addEventListener('change', function() { updateMilestoneOptions(this.value); }); });
步骤3:添加AJAX接口返回Milestone数据
from django.http import JsonResponse from django.contrib.admin.views.decorators import staff_member_required from .models import Milestone @staff_member_required def get_milestones_by_client(request): client_id = request.GET.get('client_id') # 返回id和name字段,供JS渲染选项 milestones = Milestone.objects.filter(client_id=client_id).values('id', 'name') return JsonResponse(list(milestones), safe=False)
步骤4:在urls.py里配置接口路由
from django.contrib import admin from django.urls import path from .views import get_milestones_by_client urlpatterns = [ path('admin/', admin.site.urls), # 注意替换成你的app名称 path('admin/your_app_name/milestone/get_by_client/', get_milestones_by_client, name='get_milestones_by_client'), ]
这样新建Project时,选完Client,Task里的Milestone选项就会自动更新啦。
内容的提问来源于stack exchange,提问作者Kound

