如何在Django的ExpenseForm中实现分类选择依赖日期?
实现家庭预算应用中分类选择依赖日期的方案
问题背景
我正在开发一款家庭预算管理应用,包含Date、Expense、Category、Income等模型,模型代码如下:
class Date(models.Model): name = models.CharField(max_length = 15) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='date') class Category(models.Model): name = models.CharField(max_length=100) budget = models.DecimalField(max_digits=8, decimal_places=2) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='category') date = models.ForeignKey(Date, on_delete=models.CASCADE, related_name='date', default='Styczeń 2022') class Expense(models.Model): name = models.CharField(max_length=100) amount = models.DecimalField(max_digits=8, decimal_places=2) category = models.ForeignKey(Category, on_delete=models.CASCADE) account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="account") user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense') date = models.ForeignKey(Date, on_delete=models.CASCADE, default='Styczeń 2022') class Income(models.Model): name = models.CharField(max_length=100) amount = models.DecimalField(max_digits=8, decimal_places=2) account = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="account_income") user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='income')
每个Category都关联对应的Date,我需要在ExpenseForm中实现分类选择依赖于所选日期,即仅显示选中日期对应的分类。这是因为用户可能存在同名但不同日期的分类(如2022年1月、2月、3月的“食品”分类),若不做过滤会导致选择混淆。
当前我的ExpenseForm代码如下:
class ExpenseForm(forms.ModelForm): class Meta: model = Expense fields = ('name', 'amount', 'category', 'account', 'date',) def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['category'] = forms.ModelChoiceField(queryset=user.category.all()) self.fields['account'] = forms.ModelChoiceField(queryset=user.account.all()) self.fields['date'] = forms.ModelChoiceField(queryset=user.date.all()) self.fields['name'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'}) self.fields['amount'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'}) self.fields['category'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'}) self.fields['account'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'}) self.fields['date'].widget.attrs.update({'class': 'block w-full px-4 py-3 mb-4 border border-2 border-transparent border-gray-200 focus:outline-none rounded-none !focus:border-red-700'}) self.fields['name'].label = 'Nazwa' self.fields['amount'].label = 'Kwota' self.fields['category'].label = 'Kategoria' self.fields['account'].label = 'Konto bankowe' self.fields['date'].label = 'Zakres dat'
可行实现思路
思路一:前端AJAX动态过滤(无刷新体验)
这是用户体验最优的方案,通过JavaScript监听日期选择变化,异步请求对应分类并更新下拉选项:
调整表单初始化逻辑
修改ExpenseForm的__init__方法,根据场景(新增/编辑)初始化分类查询集:def __init__(self, user, *args, **kwargs): super().__init__(*args, **kwargs) # 初始化账户和日期字段 self.fields['account'] = forms.ModelChoiceField(queryset=user.account.all()) self.fields['date'] = forms.ModelChoiceField(queryset=user.date.all()) # 初始化分类查询集:编辑时用实例关联的日期,新增时用用户第一个日期 if self.instance.pk: selected_date = self.instance.date self.fields['category'] = forms.ModelChoiceField( queryset=user.category.filter(date=selected_date) ) else: default_date = user.date.first() self.fields['category'] = forms.ModelChoiceField( queryset=user.category.filter(date=default_date) if default_date else user.category.none() ) # 保留原有样式和标签设置...给表单元素添加ID
在日期和分类字段的widget属性中添加唯一ID,方便JS定位:self.fields['date'].widget.attrs.update({ # 原有class... 'id': 'id_expense_date' }) self.fields['category'].widget.attrs.update({ # 原有class... 'id': 'id_expense_category' })添加前端JS逻辑
在渲染表单的模板中加入以下脚本,实现动态更新分类选项:const dateSelect = document.getElementById('id_expense_date'); const categorySelect = document.getElementById('id_expense_category'); dateSelect.addEventListener('change', function() { const dateId = this.value; // 清空现有分类选项 categorySelect.innerHTML = '<option value="">请选择分类</option>'; if (!dateId) return; // 发送AJAX请求获取对应日期的分类 fetch(`/get_categories/${dateId}/`) .then(response => response.json()) .then(data => { data.forEach(category => { const option = document.createElement('option'); option.value = category.id; option.textContent = category.name; categorySelect.appendChild(option); }); }) .catch(error => console.error('获取分类失败:', error)); });后端添加AJAX接口
在views.py中新增处理分类请求的视图:from django.http import JsonResponse from django.contrib.auth.decorators import login_required from .models import Category @login_required def get_categories(request, date_id): # 仅返回当前用户对应日期的分类 categories = Category.objects.filter(date_id=date_id, user=request.user) category_list = [{'id': cat.id, 'name': cat.name} for cat in categories] return JsonResponse(category_list, safe=False)配置路由
在urls.py中添加对应路由:path('get_categories/<int:date_id>/', views.get_categories, name='get_categories'),
思路二:表单提交刷新(简单直接)
如果不需要无刷新体验,这种方案实现成本更低,通过页面刷新来更新分类选项:
修改视图逻辑
在处理表单的视图中,根据GET参数中的日期ID过滤分类:def expense_form(request): user = request.user selected_date_id = request.GET.get('date_id') form = ExpenseForm(user) if selected_date_id: try: selected_date = user.date.get(id=selected_date_id) form.fields['category'].queryset = user.category.filter(date=selected_date) # 同时设置日期字段的初始值 form.fields['date'].initial = selected_date_id except Date.DoesNotExist: pass if request.method == 'POST': # 处理表单提交逻辑... form = ExpenseForm(user, request.POST) if form.is_valid(): form.save() # 重定向或返回成功页面 return render(request, 'expense_form.html', {'form': form})模板中添加自动提交逻辑
在模板中给日期选择框添加onchange事件,触发表单提交:<form method="get"> {{ form.date }} <input type="submit" style="display: none;" id="date_submit_btn"> </form> <script> document.getElementById('id_expense_date').addEventListener('change', function() { document.getElementById('date_submit_btn').click(); }); </script>注意:这种方式会刷新整个页面,适合对交互要求不高的场景。
额外优化点
- 重写Date模型的__str__方法:让日期下拉选项显示更友好
class Date(models.Model): # 现有字段... def __str__(self): return self.name - 表单验证增强:确保提交的分类确实属于所选日期,避免前端绕过验证的情况
class ExpenseForm(forms.ModelForm): # 现有代码... def clean(self): cleaned_data = super().clean() date = cleaned_data.get('date') category = cleaned_data.get('category') if date and category: if category.date != date: raise forms.ValidationError("所选分类不属于当前日期") return cleaned_data
内容的提问来源于stack exchange,提问作者Filip Kania
相关产品推荐
相关产品推荐

