如何在Flask表单中提交同一类多对象并存储到SQLAlchemy数据库?
Flask 动态购物清单实现方案
1. 数据库模型定义
先创建SQLAlchemy模型,对应购物清单条目:
from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() class ShoppingItem(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), nullable=False) cost = db.Column(db.Float, nullable=False) amount = db.Column(db.Integer, nullable=False) sell_by = db.Column(db.Date, nullable=False)
2. WTForms 表单设计
用FieldList+FormField实现多条目表单,完全适配你的场景:
from flask_wtf import FlaskForm from wtforms import StringField, FloatField, IntegerField, DateField, FormField, FieldList, SubmitField from wtforms.validators import DataRequired, NumberRange # 单个购物条目表单 class ItemForm(FlaskForm): name = StringField(validators=[DataRequired()]) cost = FloatField(validators=[DataRequired(), NumberRange(min=0)]) amount = IntegerField(validators=[DataRequired(), NumberRange(min=1)]) sell_by = DateField(format='%Y-%m-%d', validators=[DataRequired()]) # 包含多个条目清单的表单 class ShoppingListForm(FlaskForm): items = FieldList(FormField(ItemForm), min_entries=1) # 初始显示1行 submit = SubmitField('Save Shopping List')
3. Flask 后端视图
处理表单提交、数据保存和页面渲染:
from flask import Flask, render_template, redirect, url_for from datetime import datetime app = Flask(__name__) app.config['SECRET_KEY'] = 'your-secret-key' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///shopping_list.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(app) # 初始化数据库 with app.app_context(): db.create_all() @app.route('/', methods=['GET', 'POST']) def index(): form = ShoppingListForm() if form.validate_on_submit(): # 遍历表单提交的条目,转换为SQLAlchemy对象并保存 for item_data in form.items.data: sell_by_date = datetime.strptime(item_data['sell_by'], '%Y-%m-%d').date() new_item = ShoppingItem( name=item_data['name'], cost=item_data['cost'], amount=item_data['amount'], sell_by=sell_by_date ) db.session.add(new_item) db.session.commit() return redirect(url_for('index')) return render_template('index.html', form=form) # 查看已保存的购物清单 @app.route('/view') def view_list(): items = ShoppingItem.query.all() return render_template('view.html', items=items)
4. 前端模板实现
表单页面(templates/index.html)
包含动态添加/移除条目的功能,严格遵循WTForms命名规则:
<!DOCTYPE html> <html> <head> <title>Shopping List</title> <style> .s-table { width: 100%; border-collapse: collapse; margin: 20px 0; } .s-table th, .s-table td { border: 1px solid #ddd; padding: 8px; text-align: center; } .s-table th { background-color: #f2f2f2; } .btn { padding: 8px 16px; border: none; cursor: pointer; color: white; } .add-btn { background-color: #4CAF50; } .submit-btn { background-color: #008CBA; margin-top: 10px; } .remove-btn { background-color: #f44336; } .form-control { width: 90%; padding: 4px; } </style> </head> <body> <h1>Create Shopping List</h1> <form method="POST"> {{ form.hidden_tag() }} <table class="s-table"> <thead> <tr> <th>Name</th> <th>Cost</th> <th>Amount</th> <th>Sell-By</th> <th>Action</th> </tr> </thead> <tbody id="items-container"> {% for item_form in form.items %} <tr class="item-row"> <td>{{ item_form.name(class="form-control") }}</td> <td>{{ item_form.cost(class="form-control", step="0.01") }}</td> <td>{{ item_form.amount(class="form-control") }}</td> <td>{{ item_form.sell_by(class="form-control") }}</td> <td><button type="button" class="btn remove-btn" onclick="removeRow(this)">Remove</button></td> </tr> {% endfor %} </tbody> </table> <button type="button" class="btn add-btn" onclick="addItemRow()">Add Item</button> <br> {{ form.submit(class="btn submit-btn") }} </form> <a href="{{ url_for('view_list') }}">View Saved List</a> <script> function addItemRow() { const container = document.getElementById('items-container'); const rowIndex = container.querySelectorAll('.item-row').length; const newRow = document.createElement('tr'); newRow.className = 'item-row'; // 严格按照WTForms命名规则生成name属性:items-[index]-字段名 newRow.innerHTML = ` <td><input type="text" name="items-${rowIndex}-name" class="form-control" required></td> <td><input type="number" step="0.01" name="items-${rowIndex}-cost" class="form-control" min="0" required></td> <td><input type="number" name="items-${rowIndex}-amount" class="form-control" min="1" required></td> <td><input type="date" name="items-${rowIndex}-sell_by" class="form-control" required></td> <td><button type="button" class="btn remove-btn" onclick="removeRow(this)">Remove</button></td> `; container.appendChild(newRow); } function removeRow(button) { const row = button.closest('.item-row'); row.remove(); // 移除后重新索引所有行的name属性,避免表单解析失败 document.querySelectorAll('.item-row').forEach((row, idx) => { const inputs = row.querySelectorAll('input'); inputs[0].name = `items-${idx}-name`; inputs[1].name = `items-${idx}-cost`; inputs[2].name = `items-${idx}-amount`; inputs[3].name = `items-${idx}-sell_by`; }); } </script> </body> </html>
清单查看页面(templates/view.html)
展示已保存的购物条目:
<!DOCTYPE html> <html> <head> <title>Saved Shopping List</title> <style> .s-table { width: 100%; border-collapse: collapse; margin: 20px 0; } .s-table th, .s-table td { border: 1px solid #ddd; padding: 8px; text-align: center; } .s-table th { background-color: #f2f2f2; } </style> </head> <body> <h1>Saved Shopping List</h1> <table class="s-table"> <thead> <tr> <th>Name</th> <th>Cost</th> <th>Amount</th> <th>Sell-By</th> </tr> </thead> <tbody> {% for item in items %} <tr> <td>{{ item.name }}</td> <td>{{ item.cost }}</td> <td>{{ item.amount }}</td> <td>{{ item.sell_by.strftime('%Y-%m-%d') }}</td> </tr> {% endfor %} </tbody> </table> <a href="{{ url_for('index') }}">Back to Create List</a> </body> </html>
关键说明
FieldList+FormField是处理多条目表单的标准方案,min_entries=1确保页面初始显示一行输入框。- 动态添加行时,必须严格遵循WTForms的命名格式:
字段列表名-索引-字段名,否则表单无法正确解析提交的数据。 - 移除行后重新索引所有输入框的name属性,避免出现索引断层导致表单验证失败。
- 提交表单后,遍历
form.items.data即可获取所有条目数据,转换为SQLAlchemy对象后批量保存到数据库。
内容的提问来源于stack exchange,提问作者Jacob Hancock
相关产品推荐
相关产品推荐

