I have an array of forms I want rendered in a flask blueprint called fans. I am using sqlalchemy to SQLLite during dev to persist the data and flask-wtforms to render. The issue appears to be with DecimalRangeField - if I have two or more fans and change the slider on just one, the other slider appears to move to match it, despite the data value of the DecimalRangeField being unchanged. Note: the code below is working, the issue arises when the redirect line I highlighted below is deleted.
Here is the routes.py code with the "fix" of the redirect added:
@bp.route('/', methods=['GET', 'POST'])
def fans_index():
fans = Fan.query.all()
if fans.__len__() == 0:
return redirect(url_for('fans.newfan'))
form = FanForm(request.form)
if form.validate_on_submit(): # request.method == 'POST'
for fan in fans:
if fan.name == form.name.data:
fan.swtch = form.swtch.data
fan.speed = round(form.speed.data)
db.session.commit()
return redirect(url_for('fans.fans_index')) # <-- THIS is required, why?
else: # request.method == 'GET'
pass
forms = []
for fan in fans:
form = FanForm()
form.name.data = fan.name
form.swtch.data = fan.swtch
form.speed.data = fan.speed
forms.append(form)
return render_template('fans_index.html', title='Fans!', forms=forms)
Here is the form used:
class FanForm(FlaskForm):
name = HiddenField('Name')
swtch = BooleanField('Switch', render_kw={'class': 'swtch'})
speed = DecimalRangeField('Speed', render_kw={'class': 'speed'}, validators=[DataRequired()])
submit = SubmitField('Save Fan')
And here is the html template:
<h1>Fans</h1>
<div class="container">
<div class="row">
{% for form in forms %}
<div class="col mx-1 shadow-5-strong border border-white rounded" style="max-width: 220px">
<h2 class="ms-1">{{ form.name.data }}:</h2>
<form class="mx-auto ms-3" name="{{ form.name.data }}" action="" method="post">
{{ form.hidden_tag() }}
<div>{{ form.name }}</div>
<p>
{{ form.speed.label }}: <span class="speed_display_val">{{ form.speed.data | round }}%</span><br>
{{ form.speed(min=20) }}<br>
{% for error in form.speed.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.swtch.label }} <span class="ms-3">{{ form.swtch }}</span><br>
{% for error in form.swtch.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit }}</p>
</form>
</div>
{% endfor %}
</div>
</div>
I also have some simple javascript for this page to animate the slider and do submits when a user moves the slider or clicks a checkbox for turning the fans on and off:
/* script to animate the slider value changing and post data on slider mouseup or switch click */
const values = Array.from(document.getElementsByClassName('speed_display_val'));
const speeds = Array.from(document.getElementsByClassName('speed'));
const swtches = Array.from(document.getElementsByClassName('swtch')); //Note: switch is a reserved word in JS
speeds.forEach((speed, i) => {
speed.oninput = (e) => { values[i].textContent = Math.round(e.target.value) + '%' };
speed.onmouseup = () => { speed.form.requestSubmit() };
});
swtches.forEach((swtch) => {
swtch.onclick = () => { swtch.form.requestSubmit() };
});
FlaskForm
will automatically use the values from flask.request.form
and flask.request.files
. To work around this, you can pass None
for the formdata
attribute of the form. This way, the redirect that resets flask.request.form
is no longer necessary.
Your code would then look something like this.
@bp.route('/', methods=['GET', 'POST'])
def fans_index():
if Fan.query.count() == 0:
return redirect(url_for('.newfan'))
form = FanForm(request.form)
if form.validate_on_submit():
if fan := Fan.query.filter_by(name=form.name.data).first():
fan.swtch = form.swtch.data
fan.speed = round(form.speed.data)
db.session.commit()
forms = [FanForm(formdata=None, obj=fan) for fan in Fan.query.all()]
return render_template('fans_index.html', **locals())