如何在WTForms FieldList中使用FileField?基于Flask-WTF实现文件上传
给FieldList动态表单加文件上传功能
下面一步步调整代码,实现动态文件字段的验证、上传和数据库存储:
1. 修改表单类
先创建包含文件字段的子表单,再用FieldList嵌套它,同时加上文件验证规则:
from flask_wtf import FlaskForm from wtforms import StringField, SelectField, FieldList, FormField, SubmitField from flask_wtf.file import FileField, FileRequired, FileAllowed # 子表单:单个文件条目 class FileEntryForm(FlaskForm): file = FileField('上传文件', validators=[ FileRequired(message='必须选择文件'), FileAllowed(['jpg', 'png', 'pdf'], '只支持图片和PDF') ]) description = StringField('文件说明') # 主表单:集成动态文件字段 class DynamicForm(FlaskForm): # 保留你原来的StringField、SelectField等字段 files = FieldList(FormField(FileEntryForm), min_entries=1) # 默认至少一个文件字段 submit = SubmitField('提交')
2. 更新模板宏和表单渲染
渲染字段的宏要区分FileField和普通字段,且主表单必须加enctype="multipart/form-data"才能传递文件:
字段渲染宏
{% macro render_field(field) %} <div class="form-group mb-3"> {{ field.label(class="form-label") }} {% if field.type == 'FileField' %} {{ field(class="form-control-file") }} {% else %} {{ field(class="form-control") }} {% endif %} {% for error in field.errors %} <small class="text-danger d-block mt-1">{{ error }}</small> {% endfor %} </div> {% endmacro %} {% macro render_file_entry(entry) %} <div class="form-entry border p-3 mb-3"> {{ render_field(entry.file) }} {{ render_field(entry.description) }} <button type="button" class="btn btn-sm btn-danger remove-entry">删除</button> </div> {% endmacro %}
主表单模板
<form method="POST" action="" enctype="multipart/form-data"> {{ form.hidden_tag() }} <!-- 渲染你原来的其他字段 --> <!-- 动态文件字段区域 --> <div id="file-entries"> {% for entry in form.files %} {{ render_file_entry(entry) }} {% endfor %} </div> <button type="button" id="add-file-btn" class="btn btn-primary mb-3">添加文件</button> {{ form.submit(class="btn btn-success") }} </form>
3. 调整JS动态添加逻辑
动态生成的文件字段必须符合WTForms的命名规则(比如files-0-file、files-1-file),还要处理删除后的索引重排,避免后端解析出错:
// 添加文件字段按钮事件 document.getElementById('add-file-btn').addEventListener('click', function() { const container = document.getElementById('file-entries'); const entryCount = container.children.length; // 生成符合命名规则的新条目 const newEntry = document.createElement('div'); newEntry.className = 'form-entry border p-3 mb-3'; newEntry.innerHTML = ` <div class="form-group mb-3"> <label class="form-label" for="files-${entryCount}-file">上传文件</label> <input type="file" class="form-control-file" id="files-${entryCount}-file" name="files-${entryCount}-file" required> </div> <div class="form-group mb-3"> <label class="form-label" for="files-${entryCount}-description">文件说明</label> <input type="text" class="form-control" id="files-${entryCount}-description" name="files-${entryCount}-description"> </div> <button type="button" class="btn btn-sm btn-danger remove-entry">删除</button> `; container.appendChild(newEntry); // 绑定删除按钮事件 newEntry.querySelector('.remove-entry').addEventListener('click', function() { container.removeChild(newEntry); reindexEntries(); // 删除后重排索引 }); }); // 重排表单条目索引,确保name属性连续 function reindexEntries() { const entries = document.querySelectorAll('.form-entry'); entries.forEach((entry, index) => { const fileInput = entry.querySelector('[name^="files-"]'); const descInput = entry.querySelector('[name$="-description"]'); if (fileInput) { fileInput.name = `files-${index}-file`; fileInput.id = `files-${index}-file`; } if (descInput) { descInput.name = `files-${index}-description`; descInput.id = `files-${index}-description`; } }); } // 初始化已有删除按钮的事件 document.querySelectorAll('.remove-entry').forEach(btn => { btn.addEventListener('click', function() { this.parentElement.remove(); reindexEntries(); }); });
4. 路由处理文件保存和数据库更新
在提交路由里,验证表单后逐个处理文件上传,再把文件路径存到SQLite:
import os from flask import render_template, redirect, url_for from werkzeug.utils import secure_filename from app import app, db from app.forms import DynamicForm from app.models import FileRecord # 对应你的模型 # 配置上传文件夹,确保目录存在 app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'uploads') os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) @app.route('/dynamic-form', methods=['GET', 'POST']) def dynamic_form(): form = DynamicForm() if form.validate_on_submit(): # 遍历每个动态文件条目 for entry in form.files.data: file_obj = entry['file'] desc = entry['description'] # 生成安全文件名,避免重名和路径注入 safe_filename = secure_filename(file_obj.filename) # 拼接保存路径 save_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename) # 保存文件到服务器 file_obj.save(save_path) # 把文件路径和描述存到SQLite(存相对路径更灵活) new_record = FileRecord( file_path=save_path.replace(app.root_path, '', 1), description=desc ) db.session.add(new_record) db.session.commit() return redirect(url_for('submit_success')) # 跳转到成功页 return render_template('dynamic_form.html', form=form)
5. 模型修改
更新你的SQLite模型,添加存储文件路径和描述的字段:
from app import db class FileRecord(db.Model): id = db.Column(db.Integer, primary_key=True) file_path = db.Column(db.String(255), nullable=False) # 存文件相对路径 description = db.Column(db.String(255)) # 可选的文件描述 def __repr__(self): return f'<File {self.file_path}>'
关键注意事项
- 必须给表单加
enctype="multipart/form-data",否则无法传递文件。 - 动态生成的字段name属性必须严格按
{FieldList名称}-{索引}-{子字段名称}的格式,WTForms才能正确解析。 - 用
secure_filename()处理文件名,避免路径注入等安全问题。 - 如果需要限制文件大小,可以在Flask配置里加
MAX_CONTENT_LENGTH = 16 * 1024 * 1024(比如限制16MB)。
内容的提问来源于stack exchange,提问作者Artistica Digital
相关产品推荐
相关产品推荐

