Past Lesson Note

This note is from 8 months ago. You're viewing content from a previous lesson.

Daily Note for October 3, 2025 Past Lesson

Today is Friday and we discuss careers in computing: https://www.youtube.com/watch?v=nUDcV136z1w

At the start of every class, please:

Make sure you are working from your programming folder
Make sure your python virtual environment is activated
Make sure you can access our test template at 127.0.0.1:5000

Part A

Please install extension SQLite Viewer by Florian Klampfer

Please copy and paste the latest app.py into Visual Studio Code. The latest file can be found here: https://github.com/bmackenty/25-26-9th-grade/blob/main/04-school-store/app.py

What is a database here?

We use SQLite. Everything is stored in one file: school_store.db.

Data is organized in tables (like spreadsheets).

  • users (who can log in)

  • categories (product groups)

  • products (what we sell)

  • sales (each checkout)

  • sale_items (line items inside a sale)

Primary keys and foreign keys

  • A primary key is a row’s unique ID (e.g., products.id).

  • A foreign key is a pointer to a row in another table (e.g., sale_items.sale_id points to sales.id).

  • Foreign keys are safety rails so the database doesn’t allow “nonsense links.”

CRUD

  • Create (INSERT) — add a product or a sale

  • Read (SELECT) — show inventory or stats

  • Update (UPDATE) — edit a product or reduce stock after a sale

  • Delete (DELETE) — rarely used here

JOIN (idea only)

  • JOIN lets us read from two tables at once (e.g., product + category name).

Part B — Quick checks (2 minutes)

  • Which table stores one row per checkout?

  • Which table stores items inside a sale?

  • What do foreign keys do?

Part C — Four tiny, DB-focused additions

Pre-flight (start of class)

  • Work from your programming folder.

  • Activate your virtual environment.

  • cd 25-26-9th-Grade/04-school-store

  • Run python app.py

  • Visit http://127.0.0.1:5000 and log in (admin/admin123 by default).

Task 1) Enforce — Turn on foreign key safety rails

Where: In get_db_connection() (add one line).
Edit:

def get_db_connection():
    """Create a database connection."""
    conn = sqlite3.connect(DB_NAME)
    conn.row_factory = sqlite3.Row
    # Enforce foreign key rules on every connection
    conn.execute("PRAGMA foreign_keys = ON;")
    return conn

Add a tiny check route (anywhere above if name == 'main':):

@app.route('/db/foreign_keys')
def db_foreign_keys():
    conn = get_db_connection()
    row = conn.execute("PRAGMA foreign_keys;").fetchone()
    conn.close()
    return jsonify({"foreign_keys_enforced": bool(row[0])})

Done when: Visit /db/foreign_keys → {"foreign_keys_enforced": true}.

Task 2) Read — Simple database counts (how many rows?)

Where: Add this route above if name == 'main':.
Edit:

@app.route('/db/stats')
def db_stats():
    if not session.get('user_id'):
        return jsonify({"error": "login required"}), 401
    conn = get_db_connection()
    stats = {
        "users": conn.execute("SELECT COUNT(*) FROM users").fetchone()[0],
        "categories": conn.execute("SELECT COUNT(*) FROM categories").fetchone()[0],
        "products": conn.execute("SELECT COUNT(*) FROM products").fetchone()[0],
        "sales": conn.execute("SELECT COUNT(*) FROM sales").fetchone()[0],
        "sale_items": conn.execute("SELECT COUNT(*) FROM sale_items").fetchone()[0],
    }
    conn.close()
    return jsonify(stats)

Done when: Logged in, visit /db/stats and see counts for 5 tables.

Task 3) Create (safely) — Seed three demo products only if empty

Why: Students need data to “see” DB results.
Where: Add this route above if name == 'main':.
Edit:

@app.route('/db/seed-if-empty')
def db_seed_if_empty():
    if not session.get('user_id'):
        return jsonify({"error": "login required"}), 401
    conn = get_db_connection()
    try:
        count = conn.execute("SELECT COUNT(*) FROM products").fetchone()[0]
        if count == 0:
            # Use existing categories; default data already inserted in init_database()
            conn.executemany("""
                INSERT INTO products (name, description, price, cost, stock_quantity, min_stock_level, category_id, sku)
                VALUES (?, ?, ?, ?, ?, ?, (SELECT id FROM categories WHERE name = ? LIMIT 1), ?)
            """, [
                ('Granola Bar', 'Oats and honey', 1.00, 0.35, 4, 5, 'Food & Drinks', 'SKU-GRN-001'),
                ('Notebook A5', 'Ruled, 80 pages', 3.50, 1.20, 40, 10, 'School Supplies', 'SKU-NTB-101'),
                ('Water Bottle', '750 ml', 1.50, 0.60, 2, 5, 'Food & Drinks', 'SKU-WTR-750'),
            ])
            conn.commit()
            msg = "Seeded 3 demo products."
        else:
            msg = "Products table already has data. No changes made."
        return jsonify({"success": True, "message": msg})
    except Exception as e:
        conn.rollback()
        return jsonify({"success": False, "error": str(e)})
    finally:
        conn.close()

Done when: Logged in, visit /db/seed-if-empty → it reports that it seeded (first time) or says there was already data.

Task 4) Read (filtered) — Very simple “low stock” JSON

Why: Shows a SELECT with a condition.
Where: Add this route above if name == 'main':.
Edit:

@app.route('/api/low-stock')
def api_low_stock():
    if not session.get('user_id'):
        return jsonify({"error": "login required"}), 401
    conn = get_db_connection()
    rows = conn.execute("""
        SELECT p.id, p.name, p.sku, p.stock_quantity, p.min_stock_level,
               c.name AS category_name
        FROM products p
        LEFT JOIN categories c ON p.category_id = c.id
        WHERE p.stock_quantity <= p.min_stock_level
        ORDER BY p.stock_quantity ASC, p.name ASC
        LIMIT 25
    """).fetchall()
    conn.close()
    return jsonify([dict(r) for r in rows])

Done when: Logged in, visit /api/low-stock and see JSON (if empty, lower a product’s stock_quantity to meet/beat min_stock_level, then refresh).

Task 5) Update — Restock a product (increase stock in the DB)

Why: Practice an UPDATE that changes one column in one row.
Where: Add this route above if name == 'main':.
Edit:

@app.route('/inventory/restock/', methods=['POST'])
def restock_product(product_id):
    if not session.get('user_id'):
        return redirect(url_for('login'))
    try:
        amount = int(request.form.get('amount', '0'))
    except ValueError:
        amount = 0

    if amount <= 0:
        flash('Restock amount must be a positive number.', 'error')
        return redirect(url_for('inventory'))

    conn = get_db_connection()
    conn.execute("""
        UPDATE products
        SET stock_quantity = stock_quantity + ?, updated_at = CURRENT_TIMESTAMP
        WHERE id = ?
    """, (amount, product_id))
    conn.commit()
    conn.close()

    flash(f'Restocked product #{product_id} by +{amount}.', 'success')
    return redirect(url_for('inventory'))

Template hint (very small form next to each product in inventory.html):

<form action="{{ url_for('restock_product', product_id=p['id']) }}" method="post" style="display:inline;">
  <input type="number" name="amount" min="1" value="5" style="width:4rem;">
  <button class="btn btn-sm btn-secondary" type="submit">Restock</button>
</form>

Done when: In /inventory, submit “Restock” for a product with amount “5”; after refresh, the product’s stock_quantity increases by 5 and a green success flash appears.

Task 6) Read — Export all products to CSV (download from the DB)

Why: Practice a SELECT and return data as a file.
Where: Add this route above if name == 'main':.
Edit:

import csv
from io import StringIO
from flask import Response

@app.route('/export/products.csv')
def export_products_csv():
    if not session.get('user_id'):
        return redirect(url_for('login'))

    conn = get_db_connection()
    rows = conn.execute("""
        SELECT p.id, p.name, p.sku, p.price, p.cost, p.stock_quantity, p.min_stock_level,
               c.name AS category_name, p.created_at, p.updated_at
        FROM products p
        LEFT JOIN categories c ON p.category_id = c.id
        ORDER BY p.name
    """).fetchall()
    conn.close()

    output = StringIO()
    writer = csv.writer(output)
    writer.writerow(['id','name','sku','price','cost','stock_quantity','min_stock_level','category_name','created_at','updated_at'])
    for r in rows:
        writer.writerow([
            r['id'], r['name'], r['sku'], r['price'], r['cost'],
            r['stock_quantity'], r['min_stock_level'], r['category_name'],
            r['created_at'], r['updated_at']
        ])

    csv_data = output.getvalue()
    output.close()

    return Response(
        csv_data,
        mimetype='text/csv',
        headers={'Content-Disposition': 'attachment; filename=products.csv'}
    )

Done when: Logged in, visit /export/products.csv and your browser downloads products.csv containing all products from the database.

Part D — Student checklist (5 minutes)

  • Restart the server (Ctrl+C, then python app.py).

  • Log in at http://127.0.0.1:5000.

  • Visit in order:

    • /db/foreign_keys → must say true.

    • /db/seed-if-empty → seed on first run.

    • /db/stats → counts show up.

    • /api/low-stock → shows items at/below min stock (adjust a product if needed).

    • In /inventory, use the Restock form → stock increases.

    • /export/products.csv → CSV downloads.

Exit Ticket (1 minute)

  • What do foreign keys do for us?

  • What SQL did we use to count rows in a table?

  • How did we filter for low-stock items?

  • Which SQL verb changes a value in a row?