Search code examples
pythonflaskflask-sqlalchemyflask-wtforms

Why does flask seem to require a redirect after POST?


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() };
});

Solution

  • 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())