Chapter 37 Exercises: Building Simple Business Applications with Flask
How to Use These Exercises
Five tiers of difficulty. Work through them in order — each tier builds on the previous.
Tier 1 — Recall: Verify that you understand the core concepts. Tier 2 — Apply: Build simple Flask features from a specification. Tier 3 — Extend: Add meaningful functionality to the chapter's application. Tier 4 — Design: Make architectural decisions, not just implementation choices. Tier 5 — Challenge: Production-adjacent problems requiring synthesis across chapters.
All exercises use chapter-37-flask/code/ as the starting point unless otherwise noted.
Tier 1 — Recall (Exercises 1–4)
Exercise 1: Flask Anatomy
Answer the following questions about this minimal Flask application:
from flask import Flask
app = Flask(__name__)
@app.route("/status")
def system_status():
return "All systems operational."
if __name__ == "__main__":
app.run(debug=True)
a) What does Flask(__name__) do? Why is __name__ passed rather than the string "app"?
b) What HTTP method does @app.route("/status") respond to by default?
c) What URL would you visit to see "All systems operational." in your browser, assuming the app runs on the default port?
d) What does debug=True enable that would be turned off in a production deployment?
e) If you changed the return value to "<h1>All systems operational.</h1>", how would the browser render it differently?
Exercise 2: Route Identification
Given the following Flask application, identify what each route does and what URL triggers it:
@app.route("/")
def home():
return render_template("home.html")
@app.route("/product/<int:product_id>")
def product_detail(product_id):
return f"Product ID: {product_id}"
@app.route("/search")
def search():
query = request.args.get("q", "")
return f"Searching for: {query}"
@app.route("/order", methods=["GET", "POST"])
def place_order():
if request.method == "POST":
return "Order placed"
return render_template("order_form.html")
a) What URL triggers product_detail for product ID 42?
b) What URL would trigger search and set query to "ergonomic chairs"?
c) Why does /order need methods=["GET", "POST"]?
d) What happens if you visit /product/abc (a non-integer)?
Exercise 3: Jinja2 Template Rendering
Given this route:
@app.route("/invoice/<int:invoice_id>")
def invoice_detail(invoice_id):
invoice = {
"id": invoice_id,
"client": "Hartwell Financial",
"amount": 8750.00,
"status": "Unpaid",
"line_items": [
{"description": "Strategy Session", "hours": 8, "rate": 175},
{"description": "Report Writing", "hours": 12, "rate": 175},
{"description": "Stakeholder Presentation", "hours": 4.5, "rate": 200},
],
}
return render_template("invoice.html", invoice=invoice)
Write a Jinja2 template (invoice.html) that:
- Extends base.html
- Displays the client name as a heading
- Shows the invoice total using Python's {:,.2f} format
- Loops over line_items in a table showing description, hours, and subtotal (hours × rate)
- Shows "PAID" in green or "UNPAID" in red based on invoice.status
Exercise 4: POST/Redirect/Get
Explain the Post/Redirect/Get (PRG) pattern in your own words.
a) What specific user action does PRG prevent?
b) In Flask, what functions do you use to implement PRG?
c) In the expense form in app.py, locate the line where PRG is implemented and explain what it does.
d) What would go wrong if you replaced return redirect(url_for("expense_success")) with return render_template("expense_success.html")?
Tier 2 — Apply (Exercises 5–8)
Exercise 5: Add a New Route
Add a /team route to the Acme Corp application that displays a simple team directory page. The page should show a table with the following hardcoded team members (no database required for this exercise):
| Name | Title | Department | |
|---|---|---|---|
| Sandra Chen | VP of Sales | Sales | sandra.chen@acme.com |
| Priya Okonkwo | Data Analyst | Sales | priya.okonkwo@acme.com |
| Marcus Webb | IT Manager | IT | marcus.webb@acme.com |
Requirements:
- Create a new route function named team_directory
- Render a new template team.html that extends base.html
- Pass the team data as a list of dictionaries from the route function
- Display the data in a properly formatted table using a Jinja2 for loop
- Add a navigation link in base.html to access /team
- Do not hardcode the HTML table rows — generate them with Jinja2
Exercise 6: Form Validation Improvement
The expense form currently validates that the amount is between $0 and $10,000. Extend the validation to also:
a) Reject amounts with more than 2 decimal places. $45.501` should fail; `$45.50 should pass.
b) Add a "Reason for Exception" field that is only required when the category is "Client Gifts" and the amount is over $100. If those two conditions are met and the field is empty, show an error. Otherwise, the field is optional.
c) Validate that the expense date is not more than 90 days in the past. Expenses older than 90 days require finance approval via a separate process and should not be submitted through this form.
Write only the Python validation logic (the if request.method == "POST" block). You do not need to update the template for this exercise, though you may describe what template changes would be needed.
Exercise 7: Read from CSV and Display
The file data/acme_sales.csv contains sales data with columns: deal_id, sale_date, region, sales_rep, product, revenue, status.
Add a route /deals that:
- Reads the CSV with pandas
- Filters to only Closed Won deals
- Displays the 20 most recent deals in a table, sorted by sale_date descending
- Formats the revenue column as $X,XXX
- Shows the sale_date in Month DD, YYYY format (e.g., "March 15, 2023")
- The route should be protected by the @login_required decorator
Create the route function and the template deals.html. The template must extend base.html.
Exercise 8: JSON API Endpoint
Add a JSON API endpoint to the Acme Corp application at /api/top-reps?limit=N where N is an optional query parameter (default: 5).
The endpoint should:
- Return JSON: a list of objects with sales_rep and total_revenue keys
- Sort by total_revenue descending
- Limit the results to N entries (must be between 1 and 20; reject invalid values with a 400 status code)
- Be protected by @login_required
- Return proper JSON error messages (not HTML) when the limit parameter is invalid
Example valid response:
[
{"sales_rep": "Sandra Chen", "total_revenue": 287450.00},
{"sales_rep": "James Park", "total_revenue": 254100.50}
]
Example error response (for ?limit=50):
{"error": "limit must be between 1 and 20"}
Tier 3 — Extend (Exercises 9–12)
Exercise 9: Expense Approval Workflow
Extend the expense submission system with a basic approval workflow:
a) Add a status field to the expense CSV that starts as "Pending Review".
b) Create a new route /expenses/<int:expense_id>/approve (POST method only) that updates the expense record's status to "Approved". This route should require authentication.
c) Create a route /expenses/<int:expense_id>/reject (POST method only) that sets the status to "Rejected".
d) Update the expense history page to display the current status with appropriate color coding (green for approved, red for rejected, amber for pending).
e) Add "Approve" and "Reject" buttons on the expense history page that POST to the relevant routes.
Because the data is in a CSV file (not a database), updating a record requires reading the entire file, modifying the target row, and writing the file back. Implement this correctly. Consider the thread-safety implications and document your approach.
Exercise 10: Dashboard Date Filtering
Extend the sales dashboard to support date range filtering.
a) Add a form to the dashboard page with two date inputs: "From Date" and "To Date". When submitted, these should be sent as GET parameters (not POST), so the filtered URL is shareable (e.g., /dashboard?from=2023-01-01&to=2023-06-30).
b) Modify load_sales_metrics() to accept optional start_date and end_date parameters. When provided, filter the DataFrame before computing metrics.
c) Modify the dashboard route to read from and to from request.args and pass them to load_sales_metrics().
d) Display the active date range prominently on the dashboard when a filter is applied.
e) Add a "Clear Filter" link that resets to the full year view.
Exercise 11: Template Macro for Metric Cards
The dashboard template currently has repetitive HTML for each metric card. Refactor this using a Jinja2 macro — a reusable template function.
a) Define a macro called metric_card that accepts label, value, subtext (optional), and highlight (boolean, default False).
b) Replace all metric card HTML in the dashboard template with calls to this macro.
c) Create a separate file templates/macros.html to store the macro, and import it in the dashboard template using Jinja2's {% from ... import ... %} syntax.
d) Write a brief explanation of when you would use a Jinja2 macro versus defining a route that returns a JSON fragment.
Exercise 12: Maya's Portal — Add a Contact Form
Extend Maya's client portal (from Case Study 37-2) with a "Send Message to Maya" form on the project status page.
Requirements:
- The form should appear at the bottom of the status page
- Fields: message subject (required), message body (required, min 20 chars, max 1000 chars)
- The client's name and project code should be auto-populated from the session (not editable fields)
- On submission, write the message to a messages.csv file with timestamp, project code, client name, subject, and body
- Implement the PRG pattern
- Show a success confirmation that includes the timestamp of the submission
Bonus: Add input sanitization to prevent malicious input from being stored. Specifically, strip any HTML tags from the subject and body before saving.
Tier 4 — Design (Exercises 13–16)
Exercise 13: Caching Strategy Decision
The load_sales_metrics() function reads and processes the entire CSV on every request. For a small file and low traffic, this is fine. As the application scales, it becomes a bottleneck.
a) Describe three different caching strategies you could implement in Flask: 1. Module-level cache: Store the metrics in a global variable with a timestamp, re-compute when the data is stale. 2. Flask-Caching extension: Use a proper cache backend (in-memory or Redis). 3. Server-side file watcher: Re-compute only when the CSV file's modification time changes.
b) For each strategy, describe the tradeoff between implementation complexity, correctness, and performance.
c) Priya expects the CSV to be updated once per day by an automated pipeline. Which strategy would you recommend, and why?
d) Implement the strategy you recommended in (c). Show the code changes to load_sales_metrics() and the route function.
Exercise 14: API Design for Sandra's Mobile Access
Sandra wants to access the sales dashboard from her phone. The current dashboard renders a full HTML page. On a small screen, it is usable but not ideal.
You are evaluating two approaches:
- Approach A: Build a mobile-optimized version of the dashboard template using responsive CSS
- Approach B: Build a JSON API (the /api/metrics endpoint already exists) and use JavaScript on the client side to render the data in a mobile-friendly layout
a) List three advantages and two disadvantages of each approach.
b) You have limited time — one day of development. Which approach would you choose, and what specifically would you build in that one day?
c) A third colleague suggests using a third-party dashboarding tool (Metabase, Grafana, or Retool) to build the mobile interface instead of custom code. Under what circumstances would this be the better answer? Under what circumstances would it be worse?
Exercise 15: Multi-Tenant Architecture
Maya currently has one database and one Flask app for all her clients. She is considering taking on a new type of client: a mid-sized company with 50+ employees who would want multiple team members to access a shared project portal.
This raises a question: should she keep one shared application and database, or run a separate instance per client?
a) What are the security risks of a shared database for multi-tenant access? What SQL patterns help mitigate them?
b) What is row-level security, and how would it be implemented in Python/SQLite?
c) Describe the architecture you would recommend for Maya if she scales to 50 client companies with up to 10 users each. You do not need to implement it — describe it clearly enough that a developer could implement it from your description.
d) At what client scale does Maya's current single-password project code approach break down? What is the earliest point at which she should consider migrating to proper user accounts?
Exercise 16: Error Handling Strategy
The current application has two error handlers (404 and 500). Design a comprehensive error handling strategy.
a) Identify five specific errors that could occur in the expense form route (network issue, file permissions, data type error, missing CSV column, malformed CSV row). For each, describe what should happen from the user's perspective and what should be logged.
b) Write a Flask error handler for CSVReadError — a custom exception that the application raises when load_sales_metrics() fails to read the data file. The handler should return a user-friendly page with a 503 status code, log the full exception traceback, and include the timestamp and route in the log entry.
c) Explain why you should not show the Python traceback to users in a production application, even though Flask's debug mode does exactly this in development.
d) What is the difference between logging logger.error(str(e)) and logger.exception(e)? Which is more useful for debugging?
Tier 5 — Challenge (Exercises 17–19)
Exercise 17: Full-Stack Feature — Budget Alert System
Build an automated budget alert feature for Maya's client portal.
When a client views their project status page and their hours have exceeded 80% of budget, the system should:
-
Show a prominent alert banner on the status page: "This project has used X% of its budgeted hours. Contact Maya to discuss options."
-
Log the alert event (project code, percentage, timestamp) to an
alerts.csvfile — but only once per day per project (do not log every page view when the project is over 80%). -
Add an admin endpoint (
/admin/alerts, password protected) that shows all projects currently over 80% of budget with their exact percentages.
The logging requirement (only once per day) is the challenging part. Implement it correctly without a database — use a CSV or JSON file to track the last alert timestamp per project.
Exercise 18: Flask Testing
The Acme Corp application has no tests. Write a test suite using Flask's built-in test client and the pytest framework.
Your test suite must cover:
a) Route accessibility: Verify that unauthenticated requests to /dashboard redirect to /login. Verify that authenticated requests to /dashboard return a 200 status with the word "Revenue" in the response body.
b) Form validation: Write parametrized tests for the expense form validation. Test at least five invalid inputs (empty description, future date, negative amount, amount over $10,000, non-numeric amount) and verify they each produce the expected error message.
c) Data loading: Write a test that mocks the CSV file read (using unittest.mock.patch) and verifies that load_sales_metrics() returns the correct calculated values for a known input.
d) PRG pattern: Submit the expense form with valid data and verify that the response is a redirect (302) to the success page, not a direct render of the success template.
Write the complete test file (tests/test_app.py) with all setup, fixtures, and test functions.
Exercise 19: Building Maya's API Layer
Maya wants to add a simple REST API to her client portal so her bookkeeping software can automatically retrieve the hours logged on each project at the end of each month.
Design and implement the API:
a) Design: Define three API endpoints that a bookkeeping system would need. For each, specify the URL, HTTP method, request parameters, response schema (JSON), and authentication approach.
b) Authentication: Implement API key authentication for the new endpoints. API keys should be stored in the database (not the source code), checked against the Authorization: Bearer <key> header, and logged (key prefix only, not the full key) on each use.
c) Rate limiting: Implement basic rate limiting (maximum 100 requests per hour per API key) using a simple counter stored in a dictionary in memory. Acknowledge the limitation of this approach (it resets on server restart) and describe what you would use in a production system.
d) Documentation: Write a brief API documentation page (as a Flask route at /api/docs) that describes the available endpoints, expected inputs, and example responses. The page should be rendered as HTML using a template.
Exercise Notes and Hints
Exercise 3 hint: The loop.index variable in Jinja2 gives you the current iteration number (1-based). You can compute the subtotal inside the template: {{ "{:,.2f}".format(item.hours * item.rate) }}.
Exercise 7 hint: To format a pandas date column, use .dt.strftime("%B %d, %Y") on the column after parse_dates=["sale_date"].
Exercise 9 hint: Reading, modifying, and rewriting a CSV is best done with pandas.read_csv(), modifying the DataFrame, and df.to_csv(). Use the expense's row index as the expense_id.
Exercise 11 hint: Jinja2 macros are defined with {% macro name(param1, param2='default') %}...{% endmacro %} and imported with {% from 'macros.html' import metric_card %}.
Exercise 17 hint: Check the last alert timestamp using datetime.fromisoformat(). "Only once per day" means the date portion of the timestamp must differ from today's date.
Exercise 18 hint: Flask test client is created with app.test_client(). To simulate an authenticated session: with client.session_transaction() as sess: sess['authenticated'] = True.
Exercise 19 hint: API key authentication with Authorization: Bearer header reads the key with request.headers.get('Authorization', '').removeprefix('Bearer ').strip().