Django表单下拉框值在request.POST存在但clean方法中缺失求助
问题:Django表单clean方法中Category和Segment字段值缺失
开发Django应用时,实现了支持选择已有分类/细分或创建专属新项的Line Item表单。提交时request.POST能正确获取Category和Segment的值:
request.POST: <QueryDict: {'csrfmiddlewaretoken': ['zG8My4ID5S5uWn6vYesF3xxpEmfXA8DANbR9goRtpLDuqfKUMcrM4QKthdrwsth'], 'name': ['Mutual Funds'], 'category': ['investments'], 'new_category': [''], 'segment': ['investments'], 'new_segment': ['']}>
但在forms.py的clean方法中,cleaned_data里缺失这两个字段的值,仅能获取其他字段:
forms.py: {'name': 'Mutual Funds', 'new_category': '', 'new_segment': ''} Before setting - category: None, segment: None After setting - category: , segment:
相关代码
模板代码
<div class="container-fluid lineitem_form_container"> <div class="inner_holder"> <h2>Add Line Item</h2> <form method="post" action="{% url 'Dashboard:lineitems' %}" class="col-md-6"> {% csrf_token %} {{ form.non_field_errors }} <div class="mb-3"> <label for="{{ form.name.id_for_label }}" class="form-label">Line Item Name:</label> {{ form.name }} </div> <div class="mb-3"> <label for="{{ form.category.id_for_label }}" class="form-label">Category:</label> {{ form.category }} {{ form.new_category }} </div> <div class="mb-3"> <label for="{{ form.segment.id_for_label }}" class="form-label">Segment:</label> {{ form.segment }} {{ form.new_segment }} </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> <script> // JavaScript code to toggle visibility of new category/segment fields document.addEventListener('DOMContentLoaded', function () { var categoryDropdown = document.querySelector('#id_category'); var newCategoryField = document.querySelector('#id_new_category'); var segmentDropdown = document.querySelector('#id_segment'); var newSegmentField = document.querySelector('#id_new_segment'); // Initial state newCategoryField.style.display = 'none'; newSegmentField.style.display = 'none'; categoryDropdown.addEventListener('change', function () { newCategoryField.style.display = categoryDropdown.value === 'new_category' ? 'block' : 'none'; }); segmentDropdown.addEventListener('change', function () { newSegmentField.style.display = segmentDropdown.value === 'new_segment' ? 'block' : 'none'; }); }); </script> </div>
forms.py代码
class CustomTextInput(TextInput): def __init__(self, *args, **kwargs): kwargs['attrs'] = {'class': 'form-control'} super().__init__(*args, **kwargs) class LineItemForm(forms.ModelForm): new_category = forms.CharField( max_length=255, required=False, label='New Category', widget=CustomTextInput(attrs={'class': 'form-control', 'placeholder': 'Enter new category'}), ) new_segment = forms.CharField( max_length=255, required=False, label='New Segment', widget=CustomTextInput(attrs={'class': 'form-control', 'placeholder': 'Enter new segment'}), ) class Meta: model = LineItem fields = ['name', 'category', 'segment'] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), 'category': forms.Select(attrs={'class': 'form-select'}), 'segment': forms.Select(attrs={'class': 'form-select'}), } def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) # Set the queryset for the category and segment fields based on the user self.fields['category'].queryset = Category.objects.filter(user=user) self.fields['segment'].queryset = Segment.objects.filter(user=user) # Set the choices for the category and segment fields based on the user's choices self.fields['category'].choices = Category.CATEGORY_CHOICES + [('new_category', 'Add New')] self.fields['segment'].choices = Segment.SEGMENT_CHOICES + [('new_segment', 'Add New')] # Apply Bootstrap classes to the new category and segment fields self.fields['new_category'].widget.attrs['class'] = 'form-control' self.fields['new_category'].widget.attrs['placeholder'] = 'Enter new category' self.fields['new_segment'].widget.attrs['class'] = 'form-control' self.fields['new_segment'].widget.attrs['placeholder'] = 'Enter new segment' def clean(self): cleaned_data = super().clean() print("forms.py: ", cleaned_data) print(f"Before setting - category: {cleaned_data.get('category')}, segment: {cleaned_data.get('segment')}") # Set the values for new category and new segment cleaned_data['category'] = cleaned_data.get('new_category', cleaned_data.get('category')) cleaned_data['segment'] = cleaned_data.get('new_segment', cleaned_data.get('segment')) # Print values after setting them print(f"After setting - category: {cleaned_data.get('category')}, segment: {cleaned_data.get('segment')}") # Check if either an existing category or a new one is provided if not cleaned_data.get('category'): self.add_error('category', 'Please select an existing category or provide a new one.') if not cleaned_data.get('segment'): self.add_error('segment', 'Please select an existing segment or provide a new one.') return cleaned_data
models.py代码
class Category(models.Model): CATEGORY_CHOICES = [ ('investments', 'Investments'), ('loans', 'Loans'), ('insurance', 'Insurance'), ('taxes', 'Taxes'), ] name = models.CharField(max_length=255, choices=CATEGORY_CHOICES) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self): return self.name class Segment(models.Model): SEGMENT_CHOICES = [ ('investments', 'Investments'), ('expenses', 'Expenses'), ] name = models.CharField(max_length=255, choices=SEGMENT_CHOICES) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) def __str__(self): return self.name class LineItem(models.Model): name = models.CharField(max_length=255) category = models.ForeignKey(Category, on_delete=models.CASCADE) segment = models.ForeignKey(Segment, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) date_of_creation = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name
views.py代码
@login_required def lineitems(request): initial_data = {'category': 'default_category', 'segment': 'default_segment'} print("request.POST: ", request.POST) if request.method == 'POST': form = LineItemForm(user=request.user, data=request.POST) print("errors: ", form.errors) if form.is_valid(): line_item = form.save(commit=False) line_item.user = request.user line_item.save() return redirect('Dashboard:lineitems') # Redirect to a success page or another view else: form = LineItemForm(user=request.user, initial=initial_data) return render(request, 'Dashboard/lineitems.html', {'form': form})
问题原因
Category和Segment是外键字段,默认使用ModelChoiceField,该字段会验证提交的值是否为对应模型实例的有效ID。但你在__init__方法中直接覆盖了choices,将其设置为Category.CATEGORY_CHOICES(字符串键值对)+ 新建选项,导致提交的'investments'这类字符串值无法匹配任何Category实例的ID,触发验证失败,字段被从cleaned_data中移除。
解决方案
1. 修正表单字段逻辑
将外键字段改为ChoiceField,选项由用户已有实例的ID+显示名和“新建”选项组成,再在clean方法中手动处理选择逻辑(选择已有实例或创建新实例)。
2. 修改后的forms.py代码
class CustomTextInput(TextInput): def __init__(self, *args, **kwargs): kwargs['attrs'] = {'class': 'form-control'} super().__init__(*args, **kwargs) class LineItemForm(forms.ModelForm): new_category = forms.CharField( max_length=255, required=False, label='New Category', widget=CustomTextInput(attrs={'placeholder': 'Enter new category'}), ) new_segment = forms.CharField( max_length=255, required=False, label='New Segment', widget=CustomTextInput(attrs={'placeholder': 'Enter new segment'}), ) class Meta: model = LineItem fields = ['name'] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), } def __init__(self, user, *args, **kwargs): self.user = user super().__init__(*args, **kwargs) # 生成Category选项:已有分类(ID+显示名) + 新建选项 existing_categories = [ (cat.id, cat.get_name_display()) for cat in Category.objects.filter(user=user) ] self.fields['category'] = forms.ChoiceField( choices=existing_categories + [('new_category', 'Add New')], widget=forms.Select(attrs={'class': 'form-select'}), label='Category' ) # 生成Segment选项:已有细分(ID+显示名) + 新建选项 existing_segments = [ (seg.id, seg.get_name_display()) for seg in Segment.objects.filter(user=user) ] self.fields['segment'] = forms.ChoiceField( choices=existing_segments + [('new_segment', 'Add New')], widget=forms.Select(attrs={'class': 'form-select'}), label='Segment' ) # 设置新字段样式 self.fields['new_category'].widget.attrs['class'] = 'form-control' self.fields['new_segment'].widget.attrs['class'] = 'form-control' def clean(self): cleaned_data = super().clean() category_choice = cleaned_data.get('category') segment_choice = cleaned_data.get('segment') new_category = cleaned_data.get('new_category') new_segment = cleaned_data.get('new_segment') # 处理Category字段 if category_choice == 'new_category': if not new_category.strip(): self.add_error('category', '请输入新分类名称') else: # 检查用户是否已有同名分类,不存在则创建 category, _ = Category.objects.get_or_create( user=self.user, name=new_category.strip() ) cleaned_data['category'] = category else: # 验证选择的分类是否属于当前用户 try: cleaned_data['category'] = Category.objects.get(id=category_choice, user=self.user) except Category.DoesNotExist: self.add_error('category', '无效的分类选择') # 处理Segment字段 if segment_choice == 'new_segment': if not new_segment.strip(): self.add_error('segment', '请输入新细分名称') else: segment, _ = Segment.objects.get_or_create( user=self.user, name=new_segment.strip() ) cleaned_data['segment'] = segment else: try: cleaned_data['segment'] = Segment.objects.get(id=segment_choice, user=self.user) except Segment.DoesNotExist: self.add_error('segment', '无效的细分选择') return cleaned_data
3. 验证效果
修改后,cleaned_data会正确获取到Category和Segment实例(无论是选择已有项还是创建新项),表单验证通过后可正常保存LineItem数据。
内容的提问来源于stack exchange,提问作者Sashaank
相关产品推荐
相关产品推荐

