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

如何用Django Forms创建包含多个带数量的MealItem的Meal对象?

问题描述

我需要创建一个表单,用来创建包含多个MealItem子对象的Meal对象,每个MealItem都要能单独设置quantity参数。但目前的表单和视图组合只能选择一个子对象并设置一个数量,尝试过用前缀或修改表单,只能实现选择多个MealItem,没法给每个子对象单独设置数量。


现有代码

Models

class Meal(models.Model):
    name = models.CharField(max_length=200)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    image = models.ImageField(upload_to=meal_image_upload_to, blank=True, null=True)

    def calculate_total_macros(self):
        total_kcal = 0
        total_carbohydrates = 0
        total_fats = 0
        total_proteins = 0
        total_glycemic_load = 0
        total_glycemic_index = 0
        total_weight = 0
        weighted_glycemic_index = 0

        meal_items = self.mealitem_set.all()
        for meal_item in meal_items:
            quantity = meal_item.quantity
            food_item = meal_item.food_item
            total_weight += quantity

            # 食物营养数据按每100g计算
            total_kcal += food_item.kcal * quantity/100
            total_carbohydrates += food_item.carbohydrates * quantity/100
            total_fats += food_item.fats * quantity/100
            total_proteins += food_item.proteins * quantity/100
            total_glycemic_load += food_item.glycemic_load * quantity/100
            total_glycemic_index += food_item.glycemic_index
            weighted_glycemic_index += food_item.glycemic_index * quantity

        if total_weight != 0:
            total_kcal_per_100g = round((total_kcal / total_weight) * 100, 2)
            total_carbohydrates_per_100g = round((total_carbohydrates / total_weight) * 100, 2)
            total_fats_per_100g = round((total_fats / total_weight) * 100, 2)
            total_proteins_per_100g = round((total_proteins / total_weight) * 100, 2)
            total_glycemic_load_per_100g = round((total_glycemic_load / total_weight) * 100, 2)
        else:
            total_kcal_per_100g = 0
            total_carbohydrates_per_100g = 0
            total_fats_per_100g = 0
            total_proteins_per_100g = 0
            total_glycemic_load_per_100g = 0

        average_glycemic_index = round(weighted_glycemic_index / total_weight, 2)

        return {
            'total_kcal': total_kcal,
            'total_carbohydrates': total_carbohydrates,
            'total_fats': total_fats,
            'total_proteins': total_proteins,
            'total_glycemic_load': total_glycemic_load,
            'total_kcal_per_100g': total_kcal_per_100g,
            'total_carbohydrates_per_100g': total_carbohydrates_per_100g,
            'total_fats_per_100g': total_fats_per_100g,
            'total_proteins_per_100g': total_proteins_per_100g,
            'total_glycemic_load_per_100g': total_glycemic_load_per_100g,
            'average_glycemic_index': average_glycemic_index
        }

    def __str__(self):
        return self.name


class MealItem(models.Model):
    meal = models.ForeignKey(Meal, on_delete=models.CASCADE)
    food_item = models.ForeignKey(FoodItem, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField(default=1)

    def __str__(self):
        return f"{self.food_item.name} - {self.meal.name}"

Forms

class MealItemForm(forms.ModelForm):
    class Meta:
        model = MealItem
        fields = ['food_item', 'quantity']


MealItemFormSet = forms.inlineformset_factory(Meal, MealItem, form=MealItemForm, extra=1, can_delete=True)


class MealForm(forms.ModelForm):
    class Meta:
        model = Meal
        fields = ['name', 'image']

    def __init__(self, user=None, *args, **kwargs):
        super(MealForm, self).__init__(*args, **kwargs)
        self.user = user

    def save(self, commit=True):
        meal = super(MealForm, self).save(commit=False)
        if self.user:
            meal.user = self.user
        if commit:
            meal.save()
        return meal

Views

def add_meal(request):
    if request.method == 'POST':
        form = MealForm(request.user, request.POST, request.FILES)
        formset = MealItemFormSet(request.POST, instance=Meal())

        if form.is_valid() and formset.is_valid():
            meal = form.save()
            formset.instance = meal
            formset.save()

            return redirect('meal_list')

    else:
        form = MealForm(user=request.user)
        formset = MealItemFormSet(instance=Meal())

    return render(request, 'add_meal.html', {'form': form, 'formset': formset})

Template

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    {{ formset.management_form }}
    {% for form in formset %}
        {{ form.as_p }}
    {% endfor %}
    <input type="submit" value="Add Meal">
</form>

解决方法

你的代码已经用到了inlineformset_factory,核心问题出在视图的POST请求处理逻辑上,修改如下:

修正后的视图代码

def add_meal(request):
    if request.method == 'POST':
        form = MealForm(request.user, request.POST, request.FILES)
        # 先验证主表单有效性,再绑定临时Meal实例到表单集
        if form.is_valid():
            # 创建未保存到数据库的Meal临时实例
            meal = form.save(commit=False)
            # 将临时实例传给表单集,确保关联关系正确
            formset = MealItemFormSet(request.POST, instance=meal)
            if formset.is_valid():
                # 先保存主Meal对象
                meal.save()
                # 再保存所有MealItem子对象
                formset.save()
                return redirect('meal_list')
        # 表单无效时,重新初始化表单集(保留用户输入)
        else:
            formset = MealItemFormSet(request.POST, instance=Meal())
    else:
        form = MealForm(user=request.user)
        formset = MealItemFormSet(instance=Meal())

    return render(request, 'add_meal.html', {'form': form, 'formset': formset})

额外优化:默认显示多个子表单

如果想默认显示多个空白的MealItem输入项,只需修改inlineformset_factory的extra参数,比如设置为3:

MealItemFormSet = forms.inlineformset_factory(Meal, MealItem, form=MealItemForm, extra=3, can_delete=True)

修改后,模板会默认渲染3组food_item选择框和quantity输入框,每组都可以单独设置值,提交后会自动创建多个关联到同一Meal的MealItem对象。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 10:04:59