Return Search Results with a Single, Elegant Flask Route
This is a quick guide based on some code from the Trastr open source project, a grocery price comparison web app that I’m writing with a friend. The route I’ll be breaking down today is used for finding food products by barcode or name in the database.
First, I need to import some dependencies:
from flask import (
request,
render_template,
redirect,
flash
)
from urllib.parse import quote_plus
I’ll start by defining a function and decorating it with the Flask route information.
@app.route("/find_product", methods=["GET", "POST])
def find_product():
pass
As this is a search form, I need to import a Flask WTForms class. The content is not important for this exercise, but you can check out the class here.
@app.route("/find_product", methods=["GET", "POST])
def find_product():
form = FindProductForm()
The first if statement I wrote is commonplace in form submission routes. Do one thing if the form is valid & submitted, and another if that is not the case.
@app.route("/find_product", methods=["GET", "POST])
def find_product():
form = FindProductForm()
if form.validate_on_submit():
pass
else:
pass
To answer the question of whether the form is valid, a form must first be rendered for the user to submit. This is where the tricky part of searching for results and displaying results in one route comes in. In the course of a product search, the flask route will actually be accessed twice: first to gather form data and encode into a query string using urllib.parse.quote_plus, and next to parse the query string, search the database, and display the results. This may seem like overkill at first glance, but it confers two benefits:
- Each search on the page is an atomic, independent operation. There is no user-visible “before” state and “after” state. A user can go back to the last page or reload the current page without the browser warning that they’re submitting duplicate information.
- The search page can be accessed in API fashion by encoding the query string through means other than the html form provided.
To accept query strings, I added a line to get the value of the “query” key from the URL qquery string using flask.request. For example, example.com/find_product/?query=024600010030 would yield 024600010030. I also added an if statement in the not-validated branch to check if a query string exists.
@app.route("/find_product", methods=["GET", "POST])
def find_product():
form = FindProductForm()
query = request.args.get("query")
if form.validate_on_submit():
pass
else:
if query:
pass
else:
pass
Now there are 3 possible outcomes when running this route. I have replaced the “pass” statements with comments outlining what each will do:
@app.route("/find_product", methods=["GET", "POST])
def find_product():
form = FindProductForm()
query = request.args.get("query")
if form.validate_on_submit():
# redirect to version of route with text from search box encoded into query string
else:
if query:
# check for matching results in database, and if appropriate, render results in a list under search box
else:
# render a plain search form page (this is the starting point, actually)
Here’s the fleshed out version. Pay special attention to the return statements in each branch, and don’t worry too much about the specific logic implemented in the “yes there is a query” case. That code is necessary because the database is queried differently depending on whether the user entered a product UPC or not. Also, depending on how many results are returned, there are many cases where displaying a list is not appropriate.
@app.route("/find_product", methods=["GET", "POST"])
def find_product():
form = FindProductForm()
query = request.args.get("query")
if form.validate_on_submit():
return redirect(f"/find_product?query={quote_plus(form.query.data)}")
else:
if query:
if is_upc(query):
products = (
db.session.query(Product, ProductVersion)
.join(ProductVersion)
.order_by("product_id", ProductVersion.change_dtm.desc())
.distinct("product_id")
.filter(ProductVersion.barcode == query)
.all()
)
if len(products) == 0:
return redirect(f"/import_product/{query}")
elif len(products) == 1:
return redirect(f"/view_product/{product.Product.id}")
else:
return render_template(
"find_product.html", form=form, products=products
)
else:
products = (
db.session.query(Product, ProductVersion)
.join(ProductVersion)
.order_by("product_id", ProductVersion.change_dtm.desc())
.distinct("product_id")
.filter(ProductVersion.name.ilike(query))
.all()
)
if len(products) == 0:
return render_template("find_product.html", form=form)
flash("No products found")
else:
return render_template(
"find_product.html", form=form, products=products
)
else:
return render_template("find_product.html", form=form)
To recap, if you find yourself needing to build a self-contained search and results page, the process is fairly straightforward:
- Serve the search page template
- Get the form data and encode it into a URL query string using urllib.parse.quote_plus
- Redirect to the same route, but with that query string attached
- Parse the data from the query with flask.request and search your database
- Render results from the database in a style appropriate to their count and type
Please feel free to contact me if you have any questions!