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

Django动态下拉选择框提交表单返回form-invalid问题排查

Django表单提交返回form-invalid问题排查

问题描述

我有一个包含下拉选择框的表单,页面加载时会发送AJAX请求获取当前用户所属公司的费用分类,这部分功能正常,但提交表单时会返回form-invalid。我不熟悉Django表单,恳请帮忙排查代码问题。

模型代码

class Expense(models.Model):
    slug = models.SlugField(max_length=80, unique=True)
    debit = models.DecimalField(decimal_places=2, max_digits=10)
    date = models.DateTimeField(default=timezone.now)
    category = models.CharField(max_length=50, choices=())
    remark = models.CharField(max_length=50, validators=[MinLengthValidator(3)])
    company = models.ForeignKey(Company, on_delete=models.CASCADE)

视图代码

def expense_types(request, company_id):
    company = Company.objects.filter(company_id=company_id).first()
    types = ExpenseType.objects.filter(company=company).all()
    output = []
    for typ in types:
        data = {}
        data['category'] = typ.category
        output.append(data)
    return JsonResponse({'categories': output})

# URL
path('<str:company_id>/ExpenseTypes', views.expense_types, name='get-expense-types')

模板代码

<form method="POST" action="" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form|as_crispy_errors }}
    <div class="row">
       <div class="col-md-6 col-sm-6">
           {{ form.date|as_crispy_field }}
       </div>
       <div class="col-md-6 col-sm-6">
           {{ form.debit|as_crispy_field }}
       </div>
    </div>
    <div class="row">                            
       <div class="col-md-6 col-sm-6">
           {{ form.category|as_crispy_field }}
           <input type="hidden" id="company_id", value="{{ user.company.company_id }}">
       </div>
       <div class="col-md-6 col-sm-6">
            {{ form.remark|as_crispy_field }}
       </div>
    </div>                           
    <div class="form-group">
         <button class="btn btn-outline-warning btn-block" type="submit">Save</button>
    </div>
</form>

<script type="text/javascript">
        let category = document.getElementById('id_category')
        let company_id = document.getElementById('company_id').value
        fetch('/' + company_id + '/ExpenseTypes').then(function(response){
            response.json().then(function (data){
                let optionHTML = ''
                for (let type of data.categories){
                    optionHTML += '<option value="' + type.category + '">' + type.category + '</option>'
                }
                category.innerHTML = '<option value="' + 1 + '" selected>-- Please choose expense type --</option>' + optionHTML
            })
        })
    </script>

问题分析及解决步骤

核心问题

  1. 模型字段验证冲突:模型中category字段设置了choices=()空选项集合,Django自动生成的表单会将该字段设为ChoiceField,提交的值必须在预设的choices中,但AJAX动态加载的分类不在这个空集合里,导致验证失败。
  2. 默认选项值非法:JS中设置的默认选项value="1"是数字类型,而模型category是CharField,且该值不属于任何合法分类,用户未选择分类时提交这个值会触发验证错误。

解决方案

1. 修改模型字段

去掉category字段的空choices参数,因为分类是动态加载的,不需要固定选项集合:

category = models.CharField(max_length=50)  # 移除choices=()

2. 自定义表单,替换字段类型并添加验证

创建自定义表单,将category改为普通CharField,同时添加验证确保提交的分类属于当前用户的公司:

from django import forms
from .models import Expense, ExpenseType

class ExpenseForm(forms.ModelForm):
    # 替换默认的ChoiceField为CharField
    category = forms.CharField(max_length=50, label="费用分类")

    class Meta:
        model = Expense
        fields = ['date', 'debit', 'category', 'remark', 'company']
        # 若company字段不需要用户输入,可设置为隐藏字段
        widgets = {
            'company': forms.HiddenInput()
        }

    def clean_category(self):
        category = self.cleaned_data.get('category')
        company = self.cleaned_data.get('company')
        # 验证分类是否属于当前公司
        if not ExpenseType.objects.filter(company=company, category=category).exists():
            raise forms.ValidationError("请选择有效的费用分类")
        return category

3. 修正JS默认选项值

将默认选项的value设为空字符串,避免提交非法值,同时提示用户选择分类:

let category = document.getElementById('id_category')
let company_id = document.getElementById('company_id').value
fetch('/' + company_id + '/ExpenseTypes').then(function(response){
    response.json().then(function (data){
        let optionHTML = ''
        for (let type of data.categories){
            optionHTML += '<option value="' + type.category + '">' + type.category + '</option>'
        }
        // 默认选项设为空值,符合表单必填验证逻辑
        category.innerHTML = '<option value="" selected>-- 请选择费用类型 --</option>' + optionHTML
    })
})

4. 视图中使用自定义表单

在处理表单的视图中,初始化表单时自动设置当前用户的公司:

def add_expense(request):
    if request.method == 'POST':
        form = ExpenseForm(request.POST, instance=Expense(company=request.user.company))
        if form.is_valid():
            form.save()
            # 重定向或返回成功提示
            return redirect('expense-list')
    else:
        form = ExpenseForm(initial={'company': request.user.company})
    return render(request, 'expense_add.html', {'form': form})

内容的提问来源于stack exchange,提问作者Abdullahi Hirsi-geele

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 05:15:43