215 lines
6.1 KiB
Python
Executable File
215 lines
6.1 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():
|
|
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)
|