from flask import Flask, abort, redirect, render_template, request, url_for import datetime import json import sqlite3 import subprocess import config app = Flask(__name__) DEFAULT_CATEGORIES = [ {"name": "linux commands", "items": ["cd", "touch", "cat", "find"], "level": 1}, {"name": "types of HTML input", "items": ["radio", "password", "submit", "text"], "level": 2}, {"name": "sticky ___", "items": ["tape", "fingers", "situation", "wicket"], "level": 3}, {"name": "how i feel, spelled backwards", "items": ["live", "deliver", "desserts", "denier"], "level": 4}, ] DEFAULT_AUTHOR = "John" DEFAULT_CREATION_DATE = "15 March 2026" def today_display_date(): today = datetime.date.today() return f"{today.day} {today.strftime('%B %Y')}" def normalize_creation_date(raw_date): if not raw_date: return None text = raw_date.strip() candidate_formats = ["%Y-%m-%d", "%d %B %Y", "%d %b %Y"] for fmt in candidate_formats: try: parsed = datetime.datetime.strptime(text, fmt).date() return f"{parsed.day} {parsed.strftime('%B %Y')}" except ValueError: continue return None def get_puzzle_numbers(cursor): rows = cursor.execute("SELECT number FROM puzzle ORDER BY number").fetchall() return [row[0] for row in rows] def get_puzzle_summaries(cursor): rows = cursor.execute( "SELECT number, author, creation_date FROM puzzle ORDER BY number" ).fetchall() return [ { "number": row[0], "author": row[1] or "Unknown", "creation_date": normalize_creation_date(row[2]) or "-", } for row in rows ] def get_puzzle_record(cursor, puzzle_number): row = cursor.execute( "SELECT author, creation_date, data FROM puzzle WHERE number = ?", [puzzle_number] ).fetchone() if not row: return None try: payload = json.loads(row[2]) categories = payload.get("categories") if isinstance(categories, list) and len(categories) == 4: return { "author": row[0] or "Unknown", "creation_date": normalize_creation_date(row[1]) or "-", "categories": categories, } except (json.JSONDecodeError, TypeError): return None return None def render_puzzle(puzzle_number): with sqlite3.connect(config.db_file) as connection: cursor = connection.cursor() puzzle_numbers = get_puzzle_numbers(cursor) if puzzle_number != 0: record = get_puzzle_record(cursor, puzzle_number) if record is None: abort(404) if puzzle_number == 0: categories = DEFAULT_CATEGORIES author = DEFAULT_AUTHOR creation_date = DEFAULT_CREATION_DATE else: categories = record["categories"] author = record["author"] creation_date = record["creation_date"] return render_template( 'index.html', categories=categories, author=author, creation_date=creation_date, puzzle_numbers=puzzle_numbers, current_puzzle=puzzle_number, ) @app.route('/') def index(): return redirect(url_for('puzzle_by_number', puzzle_number=0)) @app.route('/') def puzzle_by_number(puzzle_number): return render_puzzle(puzzle_number) @app.route('/new', methods=['GET', 'POST']) def new_puzzle(): with sqlite3.connect(config.db_file) as connection: cursor = connection.cursor() puzzle_numbers = get_puzzle_numbers(cursor) default_creation_date = today_display_date() if request.method == 'GET': return render_template('new.html', creation_date=default_creation_date, puzzle_numbers=puzzle_numbers) author = request.form.get("author", "").strip() raw_creation_date = request.form.get("creation_date", "").strip() or default_creation_date creation_date = normalize_creation_date(raw_creation_date) categories = [] for i in range(1, 5): name = request.form.get(f"category_{i}_name", "").strip() words = [] for j in range(1, 5): word = request.form.get(f"category_{i}_word_{j}", "").strip() if word: words.append(word) categories.append({"name": name, "items": words, "level": i}) if not author: return render_template( 'new.html', error="Author is required.", creation_date=raw_creation_date, puzzle_numbers=puzzle_numbers, form=request.form ), 400 if not creation_date: return render_template( 'new.html', error='Creation date must be in the format "Day Month Year" (example: 15 March 2026).', creation_date=raw_creation_date, puzzle_numbers=puzzle_numbers, form=request.form ), 400 if any(not cat["name"] or len(cat["items"]) != 4 for cat in categories): return render_template( 'new.html', error="Each category needs a name and exactly 4 words.", creation_date=raw_creation_date, puzzle_numbers=puzzle_numbers, form=request.form ), 400 data = json.dumps({"categories": categories}, separators=(",", ":")) with sqlite3.connect(config.db_file) as connection: cursor = connection.cursor() cursor.execute( """ INSERT INTO puzzle (author, creation_date, data) VALUES (?, ?, ?) """, [author, creation_date, data] ) number = cursor.lastrowid return render_template( 'new.html', success=f"Saved puzzle #{number}.", creation_date=creation_date, puzzle_numbers=puzzle_numbers + [number], ) @app.route('/delete', methods=['GET', 'POST']) def delete_puzzle(): if request.method == 'POST': number = request.form.get("number", "").strip() try: puzzle_number = int(number) except ValueError: return redirect(url_for('delete_puzzle', error="invalid")) if puzzle_number <= 0: return redirect(url_for('delete_puzzle', error="invalid")) with sqlite3.connect(config.db_file) as connection: cursor = connection.cursor() cursor.execute("DELETE FROM puzzle WHERE number = ?", [puzzle_number]) deleted = cursor.rowcount > 0 if deleted: return redirect(url_for('delete_puzzle', deleted=puzzle_number)) return redirect(url_for('delete_puzzle', error="missing")) with sqlite3.connect(config.db_file) as connection: cursor = connection.cursor() puzzles = get_puzzle_summaries(cursor) return render_template( 'delete.html', puzzles=puzzles, puzzle_numbers=[puzzle["number"] for puzzle in puzzles], deleted=request.args.get("deleted"), error=request.args.get("error"), ) if __name__ == '__main__': subprocess.run(["python3", "make_db.py"], check=True) app.run(host='0.0.0.0', port=8000)