我使用 WTForms FieldList、宏和 Javascript 创建了动态字段。
使用此代码验证许多字段(StringField、SelectField 等),我可以动态添加许多字段,它们验证和更新我的 Sqlite3 数据库。 不过我希望现在能够动态添加文件。
用于文件上传的标准 Flask-WTF 代码如下所示: 我如何利用它在 FieldList 表单上进行验证?
if form.validate_on_submit():
f_fileA = form.inputname.data
filename_fileA = secure_filename(f_fileA.filename)
f_fileA.save(os.path.join(
UPLOAD_FOLDER, filename_fileA
))
我的代码 表格
forms.py
class ChildForm(Form):
childAinput_1 = StringField(
'ChildA input 1'
)
childAinput_2 = IntegerField(
'ChildA input 2'
)
category = SelectField(
'Category',
choices=[('cat1', 'Category 1'), ('cat2', 'Category 2')]
)
fileA = FileField(
'FileA'
)
class MainForm(FlaskForm):
parentinput_1 = StringField(
'Parent input 1'
)
parentinput_2 = StringField(
'Parent input 2'
)
childrenA = FieldList(
FormField(ChildForm),
min_entries=1,
max_entries=20
)
我的带有表单字段的 HTML 模板
form.html template
<a id="add" href="#">Add ChildA</a>
{# Show all subforms #}
<form id="childA-form" action="" method="POST" role="form">
{{ form.hidden_tag() }}
{# show parents fields #}
<div>
{{ form.parentinput_1.label }}
{{ form.parentinput_1 }}
</div>
<div>
{{ form.parentinput_2.label }}
{{ form.parentinput_2 }}
</div>
<div id="subforms-container">
{% for subform in form.childrenA %}
{{ macros.render_childA_form(subform, loop.index0) }}
{% endfor %}
</div>
<button type="submit">Send</button>
</form>
{% if form.errors %}
{{ form.errors }}
{% endif %}
{# Form template #}
{{ macros.render_childA_form(_template, '_') }}
当前表单验证(尚未添加文件上传)
routes.py
@bp.route('/', methods=['GET', 'POST'])
def index():
form = MainForm()
template_form = ChildForm(prefix='childrenA-_-')
if form.validate_on_submit():
# Create parent
new_parent = Parent(
parentinput_1 = form.parentinput_1.data,
parentinput_2 = form.parentinput_2.data
)
db.session.add(new_parent)
for childAe in form.childrenA.data:
new_childA = ChildA(**childAe)
# Add to parent
new_parent.childrenA.append(new_childA)
db.session.commit()
return render_template(
'index.html',
form=form,
_template=template_form
)
以及宏(如果有用):
macros.html
{%- macro render_childA_form(subform, index) %}
<div id="childA-{{ index }}-form" class="{% if index != '_' %}subform{% else %}is-hidden{% endif %}" data-index="{{ index }}">
<div>
{{ subform.childAinput_1.label }}
{{ subform.childAinput_1 }}
</div>
<div>
{{ subform.childAinput_2.label }}
{{ subform.childAinput_2}}
</div>
<div>
{{ subform.category.label }}
{{ subform.category }}
</div>
<div>
{{ subform.fileA.label }}
{{ subform.fileA }}
</div>
<a class="remove" href="#">Remove</a>
<hr/>
</div>
{%- endmacro %}
我的模特
models.py
class Parent(db.Model):
"""Stores parents."""
__tablename__ = 'parents'
parentinput_1 = db.Column(db.String(100))
parentinput_2 = db.Column(db.String(100))
id = db.Column(db.Integer, primary_key=True)
class ChildA(db.Model):
"""Stores childrenA of a parent."""
__tablename__ = 'childrenA'
id = db.Column(db.Integer, primary_key=True)
parent_id = db.Column(db.Integer, db.ForeignKey('parents.id'))
childAinput_1 = db.Column(db.String(100))
childAinput_2 = db.Column(db.Integer)
category = db.Column(db.String(4))
fileA = db.Column(db.String(255))
# Relationship
parent = db.relationship(
'Parent',
backref=db.backref('childrenA', lazy='dynamic', collection_class=list)
)
(模型、路线和表格实际上在一个文件中,但我将它们拆分在问题中以便于阅读)
非常感谢
即使您使用 FormFields 列表,该过程也与标准示例代码中的过程相同。
您可以迭代所有嵌套表单并查询相应 FileField 的值并保存结果文件。
这是我基于您的代码部分的简单示例。
from flask import (
Flask,
current_app,
redirect,
render_template,
url_for
)
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import (
DeclarativeBase,
Mapped
)
from typing import List
from flask_wtf import FlaskForm
from flask_wtf.file import FileField
from wtforms import FieldList, FormField, SelectField, StringField
from werkzeug.utils import secure_filename
import os
class Base(DeclarativeBase):
pass
db = SQLAlchemy(model_class=Base)
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY='your secret here',
SQLALCHEMY_DATABASE_URI='sqlite:///example.db',
UPLOAD_FOLDER=os.path.join(app.static_folder, 'uploads')
)
db.init_app(app)
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
class Parent(db.Model):
id:Mapped[int] = db.mapped_column(db.Integer, primary_key=True)
input1:Mapped[str] = db.mapped_column(db.String)
input2:Mapped[str] = db.mapped_column(db.String)
children:Mapped[List['Child']] = db.relationship(back_populates='parent')
class Child(db.Model):
id:Mapped[int] = db.mapped_column(db.Integer, primary_key=True)
input1:Mapped[str] = db.mapped_column(db.String)
category:Mapped[str] = db.mapped_column(db.String)
parent_id:Mapped[int]= db.mapped_column(db.Integer, db.ForeignKey('parent.id'), nullable=False)
parent:Mapped['Parent'] = db.relationship(back_populates='children')
with app.app_context():
db.drop_all()
db.create_all()
class ChildForm(FlaskForm):
class Meta:
csrf = False
input1 = StringField('Input 1')
category = SelectField(
'Category',
choices=[('cat1', 'Category 1'), ('cat2', 'Category 2')]
)
file = FileField('File')
class MainForm(FlaskForm):
input1 = StringField('Input 1')
input2 = StringField('Input 2')
children = FieldList(
FormField(ChildForm),
min_entries=1,
max_entries=20
)
@app.route('/', methods=['GET', 'POST'])
def index():
form = MainForm()
if form.validate_on_submit():
parent = Parent(
input1 = form.input1.data,
input2 = form.input2.data,
)
for subform in form.children:
child = Child()
subform.form.populate_obj(child)
file = subform.file.data
file.save(os.path.join(
current_app.config['UPLOAD_FOLDER'],
secure_filename(file.filename)
))
parent.children.append(child)
db.session.add(parent)
db.session.commit()
return redirect(url_for('index'))
return render_template('index.html', **locals())
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Index</title>
<style>
fieldset[name^="child-"] {
border: none;
border-top: 1px dotted #000;
padding: 0.32rem 0;
margin: 0.32rem 0;
}
</style>
</head>
<body>
<form method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div>
{{ form.input1.label() }}
{{ form.input1() }}
</div>
<div>
{{ form.input2.label() }}
{{ form.input2() }}
</div>
<div id="children">
{% for subform in form.children -%}
<fieldset name="child-{{ loop.index0 }}" style="display: flex;">
<div style="flex-grow: 1;">
{% for field in subform -%}
<div>
{{ field.label() }}
{{ field() }}
</div>
{% endfor -%}
</div>
<div>
<button type="button" class="btn-remove">-</button>
</div>
</fieldset>
{% endfor -%}
</div>
<div style="display: flex;">
<div style="flex-grow: 1;">
<button type="submit">Submit</button>
</div>
<div>
<button type="button" id="btn-add">+</button>
</div>
</div>
</form>
<script>
(function() {
const childrensDiv = document.getElementById('children');
const removeChildDiv = (event) => {
if (childrensDiv.childElementCount > 1) {
event.target.closest('fieldset[name^="child-"]').remove();
}
};
const addBtn = document.getElementById('btn-add');
addBtn.addEventListener('click', () => {
if (childrensDiv.childElementCount >= 1 && childrensDiv.childElementCount < 20) {
const newChildDiv = childrensDiv.lastElementChild.cloneNode(true);
const fields = newChildDiv.querySelectorAll('input[name^="children-"], select[name^="children-"]');
fields.forEach(field => {
const nameAttr = field.name,
newNameAttr = nameAttr.replace(
/^children-(\d+)-(\w+)$/,
(match, p1, p2) => `children-${parseInt(p1)+1}-${p2}`
);
field.id = field.name = newNameAttr;
field.value = '';
const label = newChildDiv.querySelector(`label[for="${nameAttr}"]`);
label.setAttribute('for', newNameAttr);
});
const rmBtn = newChildDiv.querySelector('.btn-remove');
rmBtn.addEventListener('click', removeChildDiv);
childrensDiv.appendChild(newChildDiv);
}
});
document.querySelectorAll('.btn-remove').forEach(btn => {
btn.addEventListener('click', removeChildDiv);
});
})();
</script>
</body>
</html>