In this guide, we'll walk through implementing some key features for your web application, including input validation, error handling, flash messages, searching, and filtering. This builds onto the CRUD features you have already implemented. Don't worry about this section until you have a working app.
Let's start with basic front-end validation using HTML5 attributes. These provide a first line of defence against invalid input.
<form action="/add_item" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name">
<label for="year">Year:</label>
<input type="number" id="year" name="year">
<button type="submit">Add Item</button>
</form>
<form action="/add_item" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name"
required minlength="2" maxlength="100">
<label for="year">Year:</label>
<input type="number" id="year" name="year"
required min="1800" max="2100">
<button type="submit">Add Item</button>
</form>
Explanation:
required: Ensures the field is not left empty.
minlength and maxlength: Set the minimum and maximum length for text input.
min and max: Set the minimum and maximum values for number input.
Try it Out:
Try using these concepts in your add/edit forms. They are a quick and easy way to add some basic input validation to your app.
While front-end validation improves user experience, we must also validate input on the server-side for security. This is because the front-end can easily be bypassed in web apps.
Without Validation:
@app.route('/add_item', methods=['POST'])
def add_item():
name = request.form.get('name')
year = request.form.get('year')
# Add item to database (code not shown)
return redirect(url_for('item_list'))
The route without validation blindly adds whatever is received directly into the database.
The route with validation uses similar checks for validation that you have used before:
is name empty?
is name between 2 and 100 characters?
is year an integer?
is year between 1800 and 2100?
All errors are added to a list and then we will use a module called 'flash' to show them to the user - more on this in the next section.
With Validation:
@app.route('/add_item', methods=['POST'])
def add_item():
name = request.form.get('name')
year = request.form.get('year')
errors = [] # List of errors encountered
if not name or len(name) < 2 or len(name) > 100:
errors.append("Name must be between 2 and 100 characters.")
try:
year = int(year)
if year < 1800 or year > 2100:
errors.append("Year must be between 1800 and 2100.")
except ValueError:
errors.append("Invalid year format.")
if errors:
error_message = "".join(errors) # Join error list
flash(error_message, 'danger')
return redirect(url_for('add_item_form'))
# If validation passes, add item to database (code not shown)
flash('Item added successfully!', 'success')
return redirect(url_for('item_list'))
Flash messages are a way to send one-time notifications to the user. They can be useful for providing feedback on actions like form submissions, successful updates, or error messages. Let's set them up with Bootstrap for styling.
This code is added to your base template, typically just inside the <body> tag, to handle flash messages efficiently. It checks for any flashed messages and displays them as Bootstrap alerts, making it easy for users to understand what happened. The category (e.g., 'danger' for errors or 'success' for successful actions) is used to control the styling of the alert, ensuring users can easily differentiate between types of messages.
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
To make flash() work properly, you also need to set up a secret key in your app.py file. This secret key is used by Flask to cryptographically sign session data, which is necessary for storing flash messages securely. You can put anything in as the secret key.
app.py
app = Flask(__name__)
app.secret_key = 'your_secret_key_here'
@app.route('/some_route')
def some_route():
# ... some code ...
flash('Operation successful!', 'success')
# ... more code ...
Let's implement a basic search function. This search form sends the search query as a query parameter (?query=value) to the main '/' route:
<form action="{{ url_for('index') }}" method="get" class="d-flex">
<input class="form-control me-2" type="search" name="query" placeholder="Search..." aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
Modify the main route to handle the search query using SQLAlchemy.
@app.route('/', methods=['GET'])
def get_items():
search_query = request.args.get('query')
if search_query:
# If there's a search query, filter the results
items = YourModel.query.filter(YourModel.title.ilike(f'%{search_query}%')).all()
else:
# If no search query, return all items
items = YourModel.query.all()
return render_template('index.html', items=items)
Explanation:
Let's break down how the search works:
request.args.get('query', ''): Grabs the search term from the form and stores it in search_query
.filter(): Adds a condition to the query, like a WHERE clause in SQL.
YourModel.title.ilike(f'%{search_query}%'): Searches the title column for the search_query - rename this to suit
ilike(): Performs a case-insensitive pattern match
We will add filtering functionality to the main page (index.html). Filtering could be done through a small form just above the main grid of items, allowing users to apply multiple filters. For example:
<form action="{{ url_for('get_items') }}" method="get" class="mb-4">
<select name="genre" class="form-select">
<option value="Fiction">Fiction</option>
<option value="Non-Fiction">Non-Fiction</option>
</select>
<input type="number" name="year" class="form-control mt-2" placeholder="Filter by year">
<button type="submit" class="btn btn-primary mt-2">Filter</button>
</form>
Update the main route to apply filters based on the query parameters received, using SQLAlchemy. Building on from the search query, we use a similar technique to filter by other attributes such as 'genre' or 'year'.
@app.route('/', methods=['GET'])
def get_items():
# Get filter parameters from the request
search_query = request.args.get('query', '')
genre = request.args.get('genre', '')
year = request.args.get('year', '')
# Start with all items
items = Item.query
# Apply filters if they are provided
if search_query:
items = items.filter(Item.name.ilike(f'%{search_query}%'))
if genre:
items = items.filter_by(genre=genre)
if year:
items = items.filter_by(year=year)
# Get the final list of items
items = items.all()
return render_template('index.html', items=items)
Explanation:
Let's break down the process:
items = Item.query: Start with all items (but don't fetch them yet).
items = items.filter_by(genre=genre): If a genre is specified, add this condition (but still don't fetch).
items = items.filter_by(year=year): If a year is specified, add this condition too (still not fetching).
items = items.all(): Now, fetch all items that match these conditions.