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

如何用Django ModelForm实现带optgroup的嵌套下拉选择框?

解决Django ModelForm中GroupedModelChoiceField显示注释count值的问题

看起来你已经走对了方向——用GroupedModelChoiceField实现分组下拉,现在就差把annotate出来的count值加到选项标签里了。核心思路就是重写GroupedModelChoiceField的label_from_instance方法,因为默认的标签生成逻辑只会用模型的__str__方法,不会包含我们自定义的注释字段。

下面是具体的实现步骤和代码示例:

1. 自定义带count显示的分组选择字段

首先创建一个继承自GroupedModelChoiceField的自定义字段,重写标签生成逻辑,把count值嵌入到选项文本中:

from django.forms import GroupedModelChoiceField

class FilmGroupedChoiceField(GroupedModelChoiceField):
    def label_from_instance(self, obj):
        # obj是包含count注释的Film实例,直接拼接需要的文本即可
        return f"{obj.manufacturer} - {obj.name} ({obj.count} rolls)"

2. 构建表单并动态传入查询集

因为你的查询集需要依赖owner和camera.format这两个外部参数,所以在表单的__init__方法里动态设置查询集和分组规则:

from django import forms
from django.db.models import Count
from .models import Film, Camera

class LoadCameraForm(forms.Form):
    # 先初始化空查询集,后续在__init__里填充
    film = FilmGroupedChoiceField(queryset=Film.objects.none(), required=True)
    # 如果不需要用户选择相机,可以把相机字段设为隐藏或禁用
    camera = forms.ModelChoiceField(queryset=Camera.objects.none(), required=True)

    def __init__(self, owner, target_camera, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        # 生成你需要的roll_counts查询集,和手动模板里的逻辑一致
        roll_counts = Film.objects\
            .filter(roll__owner=owner, roll__status='storage')\
            .filter(format=target_camera.format)\
            .annotate(count=Count('name'))\
            .order_by('type')
        
        # 给film字段设置查询集,同时指定分组规则为type的显示名称
        self.fields['film'].queryset = roll_counts
        self.fields['film'].group_by = lambda obj: obj.get_type_display()
        
        # 锁定目标相机,不让用户修改
        self.fields['camera'].queryset = Camera.objects.filter(id=target_camera.id)
        self.fields['camera'].widget.attrs['disabled'] = True

3. 视图中处理表单逻辑

表单只负责展示数据,实际的Roll和Camera模型修改逻辑放在视图里:

from django.shortcuts import get_object_or_404, redirect
from django.views import View
from django.shortcuts import render
from .models import Camera, Roll
from .forms import LoadCameraForm

class LoadCameraView(View):
    def get(self, request, camera_id):
        # 获取当前用户的目标相机
        camera = get_object_or_404(Camera, id=camera_id, owner=request.user)
        # 初始化表单,传入owner和camera参数
        form = LoadCameraForm(owner=request.user, target_camera=camera)
        return render(request, 'load_camera.html', {'form': form, 'camera': camera})
    
    def post(self, request, camera_id):
        camera = get_object_or_404(Camera, id=camera_id, owner=request.user)
        form = LoadCameraForm(owner=request.user, target_camera=camera, data=request.POST)
        
        if form.is_valid():
            selected_film = form.cleaned_data['film']
            # 找到该用户下对应胶片的可用胶卷(这里取第一个,你可以根据需求调整)
            available_roll = Roll.objects.filter(
                owner=request.user,
                film=selected_film,
                status='storage'
            ).first()
            
            if available_roll:
                # 更新胶卷的关联相机和状态
                available_roll.camera = camera
                available_roll.status = 'loaded'
                available_roll.save()
                # 更新相机状态为已装胶卷
                camera.status = 'loaded'
                camera.save()
                return redirect('camera_detail', camera_id=camera.id)
            else:
                # 如果没有可用胶卷,添加表单错误提示
                form.add_error('film', '该胶片没有可用的库存胶卷')
        
        return render(request, 'load_camera.html', {'form': form, 'camera': camera})

4. 模板渲染表单

模板里直接渲染表单即可,GroupedModelChoiceField会自动生成带<optgroup>的下拉框,不需要再手动写{% regroup %}:

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">装胶卷</button>
</form>

关键注意点

  • 确保你的roll_counts查询集确实包含count注释字段,不要在后续的查询操作中丢失这个字段(比如不要重新过滤后忘记加annotate)。
  • 如果你的Film模型__str__方法已经包含厂商和名称,也可以在label_from_instance里复用:return f"{str(obj)} ({obj.count} rolls)",减少重复代码。

内容的提问来源于stack exchange,提问作者Trey Piepmeier

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 06:37:56