connections/app/app.py

206 lines
5.7 KiB
Python
Executable File

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('/<int:puzzle_number>')
def puzzle_by_number(puzzle_number):
return render_puzzle(puzzle_number)
@app.route('/new', methods=['GET', 'POST'])
def new_puzzle():
default_creation_date = today_display_date()
if request.method == 'GET':
return render_template('new.html', creation_date=default_creation_date)
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,
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,
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,
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
)
@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,
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)