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

如何利用Django表单处理多模型数据,实现健身应用训练计划创建?

嘿,这个场景我之前做类似健身类项目的时候刚好遇到过!Django里处理多关联模型的表单提交,最顺手的方案就是用内联表单集(Inline Formset),完美匹配你的「一个训练计划对应多个带参数动作」的一对多关联场景。下面我一步步给你拆解实现步骤:

1. 先确认你的模型结构(确保关联关系正确)

先把你的模型补全细节,重点是给WorkoutExercises设置related_name,方便后续表单集关联:

from django.db import models
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

class Exercise(models.Model):
    name = models.CharField(max_length=100, verbose_name="动作名称")
    description = models.TextField(verbose_name="动作描述")

    def __str__(self):
        return self.name

class Workout(models.Model):
    name = models.CharField(max_length=100, verbose_name="计划名称")
    publisher = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="发布者")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")

    def __str__(self):
        return self.name

class WorkoutExercises(models.Model):
    workout = models.ForeignKey(Workout, on_delete=models.CASCADE, 
                                related_name='linked_exercises', verbose_name="所属计划")
    exercise = models.ForeignKey(Exercise, on_delete=models.CASCADE, verbose_name="训练动作")
    sets = models.IntegerField(verbose_name="组数")
    reps = models.IntegerField(verbose_name="次数")

    class Meta:
        unique_together = ('workout', 'exercise')  # 避免同一计划重复添加同一动作

    # 自定义验证:组数和次数必须大于0
    def clean(self):
        super().clean()
        if self.sets <= 0:
            raise ValidationError("组数必须大于0")
        if self.reps <= 0:
            raise ValidationError("次数必须大于0")

2. 创建表单与内联表单集

我们需要两个部分:一个用于训练计划基础信息的ModelForm,一个关联训练动作的内联表单集:

from django import forms
from django.forms import inlineformset_factory
from .models import Workout, WorkoutExercises, Exercise

class WorkoutForm(forms.ModelForm):
    class Meta:
        model = Workout
        fields = ['name']  # 发布者和创建时间由后台自动赋值,不需要用户输入

# 生成内联表单集:关联Workout和WorkoutExercises
WorkoutExerciseFormSet = inlineformset_factory(
    Workout,
    WorkoutExercises,
    fields=['exercise', 'sets', 'reps'],
    extra=3,  # 默认显示3个动作输入框,用户可以添加更多
    can_delete=True,  # 允许用户删除不需要的动作
    widgets={
        'exercise': forms.Select(attrs={'class': 'form-control'}),
        'sets': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
        'reps': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
    }
)

3. 视图函数处理表单提交

核心逻辑是:先验证训练计划表单,再验证动作表单集,全部通过后按顺序保存(先存计划,再存关联的动作):

from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .forms import WorkoutForm, WorkoutExerciseFormSet

@login_required
def create_workout(request):
    if request.method == 'POST':
        # 初始化训练计划表单
        workout_form = WorkoutForm(request.POST)
        # 初始化动作表单集:用临时Workout实例关联
        formset = WorkoutExerciseFormSet(request.POST, instance=Workout())

        if workout_form.is_valid() and formset.is_valid():
            # 先保存训练计划,设置发布者为当前登录用户
            workout = workout_form.save(commit=False)
            workout.publisher = request.user
            workout.save()

            # 把表单集关联到已保存的训练计划,然后保存
            formset.instance = workout
            formset.save()

            # 跳转到训练计划详情页(你需要自己定义这个URL)
            return redirect('workout_detail', pk=workout.pk)
    else:
        # GET请求:初始化空表单和表单集
        workout_form = WorkoutForm()
        formset = WorkoutExerciseFormSet(instance=Workout())

    context = {
        'workout_form': workout_form,
        'formset': formset,
    }
    return render(request, 'create_workout.html', context)

4. 模板渲染表单(带动态添加动作功能)

模板里必须包含表单集的management_form(Django用来管理表单数量的隐藏字段),还可以加一段JS让用户动态添加更多动作输入框:

<!-- templates/create_workout.html -->
<h1>创建新训练计划</h1>
<form method="post" class="mt-4">
    {% csrf_token %}

    <!-- 训练计划名称输入 -->
    <div class="mb-3">
        {{ workout_form.name.label_tag }}
        {{ workout_form.name }}
        {% if workout_form.name.errors %}
            <div class="text-danger mt-1">{{ workout_form.name.errors }}</div>
        {% endif %}
    </div>

    <!-- 训练动作表单集 -->
    <h3 class="mt-5">添加训练动作</h3>
    {{ formset.management_form }}
    {% for form in formset %}
        <div class="exercise-form border p-3 mb-3 rounded">
            {{ form.id }} <!-- 隐藏字段,必须保留 -->
            <div class="mb-3">
                {{ form.exercise.label_tag }}
                {{ form.exercise }}
            </div>
            <div class="row">
                <div class="col-md-6 mb-3">
                    {{ form.sets.label_tag }}
                    {{ form.sets }}
                </div>
                <div class="col-md-6 mb-3">
                    {{ form.reps.label_tag }}
                    {{ form.reps }}
                </div>
            </div>
            {% if formset.can_delete %}
                <div class="form-check">
                    {{ form.DELETE }} {{ form.DELETE.label_tag }}
                </div>
            {% endif %}
            {% if form.errors %}
                <div class="text-danger mt-2">{{ form.errors }}</div>
            {% endif %}
        </div>
    {% endfor %}

    <!-- 动态添加动作按钮 -->
    <button type="button" id="add-exercise-btn" class="btn btn-secondary mb-3">添加更多动作</button>

    <button type="submit" class="btn btn-primary">保存训练计划</button>
</form>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const addBtn = document.getElementById('add-exercise-btn');
    const formContainer = document.querySelector('form');
    const exerciseFormTemplate = document.querySelector('.exercise-form').cloneNode(true);

    addBtn.addEventListener('click', function(e) {
        e.preventDefault();
        // 获取当前表单数量
        const formCount = parseInt(document.getElementById('id_linked_exercises-TOTAL_FORMS').value);
        // 复制模板表单
        const newForm = exerciseFormTemplate.cloneNode(true);

        // 更新新表单的字段ID和name属性
        newForm.querySelectorAll('input, select').forEach(field => {
            const oldName = field.getAttribute('name');
            const newName = oldName.replace('-0-', `-${formCount}-`);
            const oldId = field.getAttribute('id');
            const newId = oldId.replace('-0-', `-${formCount}-`);
            
            field.setAttribute('name', newName);
            field.setAttribute('id', newId);
            field.value = ''; // 清空输入值
        });

        // 移除模板里的错误信息
        newForm.querySelectorAll('.text-danger').forEach(el => el.remove());

        // 插入新表单到按钮上方
        formContainer.insertBefore(newForm, addBtn);

        // 更新表单计数
        document.getElementById('id_linked_exercises-TOTAL_FORMS').value = formCount + 1;
    });
});
</script>

几个关键注意点

  • 权限控制:用@login_required确保只有登录用户能创建计划,并且强制把publisher设为当前用户,避免前端篡改。
  • 表单验证:已经在WorkoutExercises模型里加了自定义验证,确保组数和次数合法;如果需要更复杂的验证(比如同一动作不能重复添加),可以在表单集的clean方法里扩展。
  • 用户体验:动态添加动作的JS是可选的,但能大幅提升用户体验,不用刷新页面就能添加更多动作。

这样就能实现用户创建训练计划并添加多个带组数次数的动作啦!如果还有细节需要调整,比如自定义样式或者特殊验证逻辑,随时可以优化~

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:28:01