multistep forms - pkirlin/lab-flask GitHub Wiki

Multistep forms - in two different ways!

In the previous section, you saw how to use hidden variables to save application state (basically, variables) across multiple webpage requests and responses. Now, we will use our new knowledge to re-write our random number generator using a multistep form. In other words, we will have one page that asks for the lower limit, then a separate page that asks for the upper limit, then a third page that generates the random number. In the real world, there's no particular reason to break it up this way, but you will see a new technique to manage what "step" of the form you are on.

When creating multistep forms, one way to break up the steps is to have a different route and/or HTML template for each step. Try making three HTML templates:

random1.html

<html>
  <head>
    <title>Random</title>
  </head>
<body>
  <h1>Random numbers are fun!</h1>  
  <form action="{{ url_for('random2') }}" method="post">
  Pick a lower limit: <input type="text" name="lowerlim"><br>
  <input type="submit" value="Next">
  </form>
</body>
</html>

random2.html

<html>
  <head>
    <title>Random</title>
  </head>
<body>
  <h1>Random numbers are fun!</h1>  
  <form action="{{ url_for('random3') }}" method="post">
  Thanks, you picked {{low}}.<br>
  Pick an upper limit: <input type="text" name="upperlim"><br>
  <input type="hidden" name="lowerlim" value="{{low}}">
  <input type="submit" value="Generate">
  </form>
</body>
</html>

random3.html

<html>
  <head>
    <title>Random</title>
  </head>
<body>
  <h1>Random numbers are fun!</h1>  
  Your random number is: {{number}}.<br>
</body>
</html>

Notice how instead of having a single HTML template handling all of the logic, we now have three templates, one for each step of the process:

  • random1.html displays the form for getting the lower limit. The form uses url_for to pass the submitted information to the random2 route in Flask.
  • random2.html displays the form for getting the upper limit, along with confirmation of the lower limit. Similarly to the previous page, the form passes the information along to the random3 route.
  • random3.html displays the final generated number

Now let's look at the Flask app:

from flask import Flask, render_template, request
import random
app = Flask(__name__)

@app.route('/')
def hello_world():
  return 'Hello, World!'
    
@app.route("/random", methods=['get', 'post'])
def random1():
  return render_template("random1.html")

@app.route("/random2", methods=['get', 'post'])
def random2():
  lower = int(request.form['lowerlim'])
  return render_template("random2.html", low=lower)

@app.route("/random3", methods=['get', 'post'])
def random3():
  print(request.form)
  lower = int(request.form['lowerlim'])
  upper = int(request.form['upperlim'])
  n = random.randint(lower, upper + 1)
  return render_template("random3.html", number=n)
  • Notice how the Flask routes are synchronized with both their corresponding function names and the HTML templates that generate them. This can be handy to keep everything straight. The one place the names are not synchronized is in the HTML templates themselves (see above) --- you will notice that random1.html passes the form information to random2.html which passes it to random3.html.

A second method

Some people don't like having separate HTML templates for each step of the form. Similarly, some people don't like having separate Flask routes/Python functions for each separate step. Typically, both reasons for disliking it is that usually there is some information that the three templates or routes have in common that ends up being duplicated. For instance, notice that in all three of the HTML templates, the title is the same, as well as the h1 header. (Note that this can also be handled with nested HTML templates, which is a separate topic.) The Python code doesn't share much in common between the three routes, but this often changes when you add in things like opening a database. At any rate, here is a different way of doing things that accomplishes the same result. We will combine all three routes together into one, as well as all three HTML templates into one. These tasks can also be done individually.

Check this out:

<html>
  <head>
    <title>Random</title>
  </head>
<body>
  <h1>Random numbers are fun!</h1>
  {% if step == "choose_lower" %}
    <form action="{{ url_for('do_random') }}" method="post">
    Pick a lower limit: <input type="text" name="lowerlim"><br>
    <input type=hidden name="step" value="choose_upper">
    <input type="submit" value="Next">
    </form>
  {% elif step == "choose_upper" %}
    <form action="{{ url_for('do_random') }}" method="post">
    Thanks, you picked {{low}}.<br>
    Pick an upper limit: <input type="text" name="upperlim"><br>
    <input type="hidden" name="lowerlim" value="{{low}}">
    <input type="hidden" name="step" value="show_number">
    <input type="submit" value="Generate">
    </form>
  {% elif step == "show_number" %}
    Your random number is: {{number}}.<br>
  {% endif %}
</body>
</html>
from flask import Flask, render_template, request
import random
app = Flask(__name__)

@app.route('/')
def hello_world():
  return 'Hello, World!'
    
@app.route("/random", methods=['get', 'post'])
def do_random(): 
  # Note that we are not calling this function plain "random()" because random 
  # is the name of a package we imported.  

  # Step 1, display lower limit form
  if "step" not in request.form:
    return render_template("random.html", step="choose_lower")

  # Step 2, accept lower limit from form, display upper limit
  elif request.form["step"] == "choose_upper":
    lower = int(request.form['lowerlim'])
    return render_template("random.html", step="choose_upper", low=lower)

  # Step 3, accept lower+upper limits from form, display random number
  elif request.form["step"] == "show_number":
    lower = int(request.form['lowerlim'])
    upper = int(request.form['upperlim'])
    n = random.randint(lower, upper + 1)
    return render_template("random.html", step="show_number", number=n)

Notice how we have the same basic logic flow, but we combine all the routes and templates together.

You try it

Create a new app with a multistep form. This app will allow students to "register" for a single computer class. The first step of the form will be to pick if they are registering for the Fall or the Spring semester. Based on that decision, they should be able to pick from two different classes for each semester. If they pick Fall, let them choose from CS 141 (Comp Sci I), CS 172 (Discrete Structures), and CS 241 (Comp Sci III). If they pick Spring, let them choose from CS 142 (Comp Sci II), CS 231 (Computer Organization), and CS 330 (Operating Systems).

Suggestions:

  • You will have three steps in the form, just like the previous example. Have Flask/Python send the names of the semesters and course codes of the three available courses as keyword variables to the HTML template as needed.
  • Use HTML radio buttons to pick the semester and course title. A set of radio buttons allows only one choice among them. For instance:
<input type="radio" name="semester" value="fall">Fall Semester<br>
<input type="radio" name="semester" value="spring">Spring Semester<br>

This code displays a pair of radio buttons, labeled "Fall Semester" and "Spring Semester." Only one selection will be allowed between them because they have the same name parameter. When the form containing these buttons is submitted, the selection will appear in Flask as an entry in the request.form dictionary. That is, request.form["semester"] will either be the string "fall" or "spring".

See below for solution: .

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

.

<html>
  <head>
    <title>Class Picker</title>
  </head>
<body>
  <h1>Class Picker</h1>
  {% if step == "choose_semester" %}
    <form action="{{ url_for('class_picker') }}" method="post">
    Pick a semester: <br>
    <input type="radio" name="semester" value="fall">Fall Semester<br>
    <input type="radio" name="semester" value="spring">Spring Semester<br>
    <input type=hidden name="step" value="choose_class">
    <input type="submit" value="Next">
    </form>
  {% elif step == "choose_class" %}
    <form action="{{ url_for('class_picker') }}" method="post">
    Choose a class for the {{semester}} semester:<br>
    <input type="radio" name="course_code" value="{{course1_code}}">{{course1_title}}<br>
    <input type="radio" name="course_code" value="{{course2_code}}">{{course2_title}}<br>
    <input type="radio" name="course_code" value="{{course3_code}}">{{course3_title}}<br>
    <input type="hidden" name="semester" value="{{semester}}">
    <input type="hidden" name="step" value="do_registration">
    <input type="submit" value="Register for class">
    </form>
  {% elif step == "do_registration" %}
    You are now registered for {{course_code}}: {{course_title}}.
  {% endif %}
</body>
</html>
from flask import Flask, render_template, request
import random
app = Flask(__name__)

@app.route('/')
def hello_world():
  return 'Hello, World!'
    
@app.route("/register", methods=['get', 'post'])
def class_picker(): 
  fall_course_codes = ["cs141", "cs172", "cs241"]
  fall_course_titles = ["Computer Science I", "Discrete Structures", "Computer Science III"]
  spring_course_codes = ["cs141", "cs172", "cs241"]
  spring_course_titles = ["Computer Science II", "Computer Organization", "Operating Systems"]

  # Step 1, display form to choose a semester
  if "step" not in request.form:
    return render_template("courses.html", step="choose_semester", )

  # Step 2, accept semester from form, output form to choose class
  elif request.form["step"] == "choose_class":
    semester = request.form["semester"]

    if semester == "fall":
      return render_template("courses.html", step="choose_class", semester=semester, \
        course1_code=fall_course_codes[0], course1_title=fall_course_titles[0], \
        course2_code=fall_course_codes[1], course2_title=fall_course_titles[1], \
        course3_code=fall_course_codes[2], course3_title=fall_course_titles[2] )

    elif semester == "spring":
      return render_template("courses.html", step="choose_class", semester=semester, \
        course1_code=spring_course_codes[0], course1_title=spring_course_titles[0], \
        course2_code=spring_course_codes[1], course2_title=spring_course_titles[1], \
        course3_code=spring_course_codes[2], course3_title=spring_course_titles[2] )

  # Step 3, accept semester/course name from form, display thank you message
  elif request.form["step"] == "do_registration":
    # real world registration code goes here...
    semester = request.form['semester']
    course_code = request.form['course_code']

    # lookup course title so we can send it back to the form
    if semester == "fall":
      course_title = fall_course_titles[ fall_course_codes.index(course_code) ]
    else:
      course_title = spring_course_titles[ spring_course_codes.index(course_code) ]

    return render_template("courses.html", step="do_registration", \
      course_code=course_code, course_title=course_title)

Make sure you understand all this code before you continue.

⚠️ **GitHub.com Fallback** ⚠️