I have created dynamic fields using WTForms FieldList, macros and Javascript.
Using this code to validate many fields (StringField, SelectField etc) I can add many fields dynamically, and they validate and update my Sqlite3 database. However I want to be able to add files dynamically now.
The standard Flask-WTF code for file uploads looks like this: How can I utilisie this for validation on a FieldList form?
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
))
My code Forms
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
)
My HTML template with form fields
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, '_') }}
Current form validation (without adding in file upload yet)
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
)
And the macros if it's useful:
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 %}
My models
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)
)
(The models, routes and forms are actually in one file, but I split them in the question for ease of reading)
Thank you very much
The procedure remains the same as in the standard example code, even if you use a list of FormFields.
You can iterate over all nested forms and query the value of the respective FileField and save the resulting file.
Here is my simple example based on your code sections.
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" 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>