问题
I have a form where users can dynamically add fields to it. While submitting this form, backend only sees the fields which backend generated
#forms.py
class ExpensesForm(FlaskForm):
expense_name = StringField('Expense_Item', validators=[DataRequired()])
cost = FloatField('Cost', validators=[DataRequired()])
due_date = DateField('Due Date', format='%Y-%m-%d', validators=[DataRequired()], default=datetime.datetime.today().date())
type = SelectField('Role', choices=[('mutual', 'Mutual'),
('personal#1', 'Personal #1'),
('personal#2', 'Personal #2')
])
I'm passing this form with
return render_template('index.html', form=form, ...)
from the main.py
to index.html
All 4 fields are generated via;
<form class="form-horizontal" id="main-form" enctype=multipart/form-data role="form" method="post" action="/">
<input type="hidden" name="count" value="1"/>
{{ form.csrf_token }}
{{ form.expense_name(placeholder="Expense Name", id="expense_1", value="") }}
{{ form.cost(placeholder="Cost", id="cost_1", class="cost", value="") }}
{{ form.due_date(id="due_date_1") }}
{{ form.type(placeholder="Type", id="type_1") }}
<button id="b1" class="btn btn-info add-more" type="button">+</button>
<small>Press + to add another set of fields.</small>
<br>
<hr>
<button class="btn btn-sm btn-success" type="submit">Post Expense</button>
</form>
A JQuery snippet generates the same fields with different(unique) id's after each button press as a row of new fields after the last #type_
id.
When I hit the submit button, the backend only receives the first row not the generated ones.
What am I missing here?
UPDATE:
# main.py
@main_blueprint.route('/', methods=['GET', 'POST'])
def index():
dates = []
form = ExpensesForm(request.form)
if request.method == 'POST':
print(form.data)
# prints the following even when the browser sends more than 1 set of data:
# {'due_date': None, 'csrf_token': long_hash, 'expense_name': 'Electric', 'cost': 13.0, 'type': 'mutual'}
if form.validate_on_submit():
for n in range(len(form.expense_name.data)):
if form.expense_name.raw_data[n] != '':
data = Expenses(form.expense_name.raw_data[n].title(),
form.cost.raw_data[n],
datetime.datetime.strptime(form.due_date.raw_data[n], '%Y-%m-%d').date(),
form.type.raw_data[n].title(),
)
print(data)
db.session.add(data)
db.session.commit()
return redirect(url_for('main.index'))
expenses = db.session.query(Expenses).all()
# expenses_schema = ExpensesSchema()
# output = expenses_schema.dump(expenses).data
output = []
for i in expenses:
output.append(i.__dict__)
return render_template('index.html', form=form, expenses=output)
UPDATE 2
Since form.data
is a dict I can't have the names matching with the new fields. But even if I give unique names to the added fields the backend only displays the initial form fields with print(form.data)
but if I do;
for k, v in request.form.items():
print(k, v)
I get all the fields. Doesn't seem the right way to me. Any thoughts?
回答1:
You can only have a single form result per form submission. To be able to submit an arbitrary and unknown number of inputs, you need to restructure your form with the help of WTForm's field enclosures.
forms.py
from flask_wtf import FlaskForm
from wtforms import (
FieldList, FormField, DateField FloatField, StringField, SelectField)
from wtforms import Form as NoCsrfForm
class ExpenseItem(NoCsrfForm):
expense_name = StringField('Expense_Item', validators=[DataRequired()])
cost = FloatField('Cost', validators=[DataRequired()])
due_date = DateField('Due Date', format='%Y-%m-%d',
validators=[DataRequired()],
default=datetime.datetime.today().date())
type = SelectField('Role', choices=[
('mutual', 'Mutual'),
('personal#1', 'Personal #1'),
('personal#2', 'Personal #2'),
])
class ExpensesForm(FlaskForm):
"""A collection of expense items."""
items = FieldList(FormField(ExpenseItem), min_entries=1)
I'd strongly recommend that you preface all your field names with expense
, not just expense_name
for sanity's sake.
index.html
<form class="form-horizontal" id="main-form" enctype=multipart/form-data role="form" method="post" action="/">
<input type="hidden" name="count" value="1"/>
{{ form.hidden_tag() }}
{% for expense_item in form.items %}
{{ form.expense_name(placeholder="Expense Name", value="") }}
{{ form.cost(placeholder="Cost", class="cost", value="") }}
{{ form.due_date() }}
{{ form.type(placeholder="Type") }}
{% endfor %}
<button id="b1" class="btn btn-info add-more" type="button">+</button>
<small>Press + to add another set of fields.</small>
<br>
<hr>
<button class="btn btn-sm btn-success" type="submit">Post Expense</button>
</form>
Note that the id
attribute of the HTML input fields must follow a particular pattern. So for every new expense item field which you add by clicking on the +
button, you need to re-number the id
attribute of its input fields.
something.js
Everything else was comparatively easy. You now need to write a piece of .js which will re-index the id
attributes of all the input fields every time a new expense item is added. I accomplished this using the Zepto library for Javascript. It wasn't fun, and my .js is terrible. The best I can do here is just paste the whole thing and hope it'll be of service to you. I know it's confusing, but I was added multiple classes to a course. For you, you'll want expense_item/expense_request or whatever you go with:
// append class-box when new class link clicked
$("#new-class").click(function(event) {
appendClassBox('#classes', {{ newclass|tojson|safe }});
reindexNames('.class-box');
return false;
})
// remove class box when its "remove" link is clicked
$(document).on('click', '#remove-class', function(){
var $toremove = $(this).closest('.class-box');
$toremove.remove();
reindexNames('.class-box');
return false;
})
// add a new class-box
function appendClassBox(container, content) {
$(container).append(content);
// raise last and hence newest class box
raiseClassBox($(container).children().last())
return false;
}
function isNormalInteger(str) {
var n = ~~Number(str);
return String(n) === str && n >= 0;
}
// re-index class-box names
function reindexNames(class_name) {
var $oboxen = $(class_name);
$oboxen.each(function(index) {
// Get all the input fields in the class-box.
var $labels = $oboxen.eq(index).find('label')
var $inputs = $oboxen.eq(index).find(
'input, select, textarea')
// Update the index contained in the name attribute.
$inputs.each(function(idx) {
var $name = $inputs.eq(idx).attr('name').split('-');
// If number in name, grab from number onwards.
var $has_num = false
for (var part in $name) {
if (isNormalInteger($name[part])) {
$has_num = true
$name = $name.slice(part)
$name[0] = index
break
}
}
// Re-index.
if ($has_num == false) {
$name.unshift(index)
}
var $prefix = 'questions'
if (class_name == '.class-box') {
$prefix = 'classes'
}
$name.unshift($prefix)
if (idx > 0) {
$labels.eq(idx - 1).attr('for', $name.join('-'));
}
$inputs.eq(idx).attr('id', $name.join('-'));
$inputs.eq(idx).attr('name', $name.join('-'));
})
})
}
views.py
@main_blueprint.route('/', methods=['GET', 'POST'])
def index():
form = ExpensesForm()
# Iterate over a collection of new expense items.
if form.validate_on_submit():
for item in form.items.data:
print(item['expense_name'])
print(item['cost'])
print(item['due_date'])
print(item['type'])
来源:https://stackoverflow.com/questions/54059700/submit-wtform-with-dynamically-generated-fields