如何利用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
相关产品推荐
相关产品推荐

