Add puzzle creation
This commit is contained in:
parent
d52af8f024
commit
5f91521229
119
app/app.py
119
app/app.py
|
|
@ -1,13 +1,126 @@
|
|||
from flask import Flask, send_file, render_template, request
|
||||
from flask import Flask, abort, render_template, request
|
||||
|
||||
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},
|
||||
]
|
||||
|
||||
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_categories(cursor, puzzle_number):
|
||||
row = cursor.execute("SELECT data FROM puzzle WHERE number = ?", [puzzle_number]).fetchone()
|
||||
if not row:
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(row[0])
|
||||
categories = payload.get("categories")
|
||||
if isinstance(categories, list) and len(categories) == 4:
|
||||
return categories
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def render_puzzle(puzzle_number=None):
|
||||
connection = sqlite3.connect(config.db_file)
|
||||
cursor = connection.cursor()
|
||||
puzzle_numbers = get_puzzle_numbers(cursor)
|
||||
|
||||
if puzzle_number is None and puzzle_numbers:
|
||||
puzzle_number = puzzle_numbers[-1]
|
||||
|
||||
if puzzle_number is not None:
|
||||
categories = get_puzzle_categories(cursor, puzzle_number)
|
||||
if categories is None and puzzle_number in puzzle_numbers:
|
||||
categories = DEFAULT_CATEGORIES
|
||||
elif categories is None:
|
||||
connection.close()
|
||||
abort(404)
|
||||
else:
|
||||
categories = DEFAULT_CATEGORIES
|
||||
|
||||
connection.close()
|
||||
return render_template(
|
||||
'index.html',
|
||||
categories=categories,
|
||||
puzzle_numbers=puzzle_numbers,
|
||||
current_puzzle=puzzle_number,
|
||||
)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
return render_puzzle()
|
||||
|
||||
@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():
|
||||
if request.method == 'GET':
|
||||
return render_template('new.html', creation_date=datetime.date.today().isoformat())
|
||||
|
||||
author = request.form.get("author", "").strip()
|
||||
creation_date = request.form.get("creation_date", "").strip() or datetime.date.today().isoformat()
|
||||
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=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=creation_date,
|
||||
form=request.form
|
||||
), 400
|
||||
|
||||
data = json.dumps({"categories": categories}, separators=(",", ":"))
|
||||
connection = sqlite3.connect(config.db_file)
|
||||
cursor = connection.cursor()
|
||||
row = cursor.execute("SELECT COALESCE(MAX(number), 0) + 1 FROM puzzle").fetchone()
|
||||
number = row[0]
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO puzzle (number, author, creation_date, data)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""",
|
||||
[number, author, creation_date, data]
|
||||
)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
return render_template(
|
||||
'new.html',
|
||||
success=f"Saved puzzle #{number}.",
|
||||
creation_date=creation_date
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
subprocess.run(["python3", "make_db.py"])
|
||||
subprocess.run(["python3", "make_db.py"], check=True)
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
db_dir = "/connections/db"
|
||||
db_file = f"{db_dir}/data.db"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import os
|
||||
import sqlite3
|
||||
|
||||
import config
|
||||
|
||||
os.makedirs(config.db_dir, exist_ok=True)
|
||||
connection = sqlite3.connect(config.db_file)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS puzzle(
|
||||
number INTEGER PRIMARY KEY,
|
||||
author TEXT,
|
||||
creation_date TEXT,
|
||||
data CLOB
|
||||
)
|
||||
"""
|
||||
)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
|
@ -19,6 +19,26 @@ h1 {
|
|||
margin-bottom: 20px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.header-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.header-links a {
|
||||
border: 1px solid #000;
|
||||
border-radius: 999px;
|
||||
padding: 6px 10px;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
.header-links a.active {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
#grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
|
@ -84,3 +104,82 @@ button:disabled {
|
|||
height: 24px;
|
||||
color: #d33;
|
||||
}
|
||||
#message.ok {
|
||||
color: #2b7d32;
|
||||
}
|
||||
.subtitle {
|
||||
margin-top: -8px;
|
||||
margin-bottom: 18px;
|
||||
font-weight: bold;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.builder {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
||||
.meta-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.meta-row label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.meta-row input {
|
||||
border: 1px solid #b8b8ae;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.builder-grid {
|
||||
max-width: 700px;
|
||||
}
|
||||
.builder-row {
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
.builder-row > input {
|
||||
border: 1px solid #8f8f8f;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.builder-words {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
.builder-words input {
|
||||
border: 1px solid #8f8f8f;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.ghost-link {
|
||||
padding: 12px 24px;
|
||||
border-radius: 25px;
|
||||
border: 1px solid #000;
|
||||
background: white;
|
||||
color: #000;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
line-height: 22px;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.meta-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.builder-words {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="header-links">
|
||||
<a href="/">/</a>
|
||||
{% for number in puzzle_numbers %}
|
||||
<a href="/{{ number }}" class="{{ 'active' if current_puzzle == number else '' }}">/{{ number }}</a>
|
||||
{% endfor %}
|
||||
<a href="/new">/new</a>
|
||||
</nav>
|
||||
|
||||
<h1>Connections</h1>
|
||||
|
||||
|
|
@ -23,13 +30,7 @@
|
|||
<div id="message"></div>
|
||||
|
||||
<script>
|
||||
// EDIT YOUR CATEGORIES HERE
|
||||
const 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}
|
||||
];
|
||||
const categories = {{ categories | tojson }};
|
||||
|
||||
let words = categories.flatMap(cat => cat.items).sort(() => Math.random() - 0.5);
|
||||
let selected = [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Create Connections Puzzle</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Connections</h1>
|
||||
<p class="subtitle">Create Puzzle</p>
|
||||
|
||||
<form method="post" class="builder">
|
||||
<div class="meta-row">
|
||||
<label>
|
||||
Author
|
||||
<input type="text" name="author" value="{{ (form.author if form else '') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Creation Date
|
||||
<input type="date" name="creation_date" value="{{ (form.creation_date if form else creation_date) }}" required>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="grid" class="builder-grid">
|
||||
{% for i in range(1, 5) %}
|
||||
<div class="card solved builder-row" style="background-color: var(--lvl-{{ i }})">
|
||||
<input
|
||||
type="text"
|
||||
name="category_{{ i }}_name"
|
||||
placeholder="Category {{ i }} name"
|
||||
value="{{ (form['category_' ~ i ~ '_name'] if form else '') }}"
|
||||
required
|
||||
>
|
||||
<div class="builder-words">
|
||||
{% for j in range(1, 5) %}
|
||||
<input
|
||||
type="text"
|
||||
name="category_{{ i }}_word_{{ j }}"
|
||||
placeholder="Word {{ j }}"
|
||||
value="{{ (form['category_' ~ i ~ '_word_' ~ j] if form else '') }}"
|
||||
required
|
||||
>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button type="submit">Save Puzzle</button>
|
||||
<a class="ghost-link" href="/">Back to Puzzle</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="message" class="{{ 'ok' if success else '' }}">
|
||||
{% if error %}{{ error }}{% elif success %}{{ success }}{% endif %}
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -7,3 +7,5 @@ services:
|
|||
stop_signal: SIGINT
|
||||
ports:
|
||||
- '8003:8000'
|
||||
volumes:
|
||||
- "./db:/connections/db"
|
||||
|
|
|
|||
Loading…
Reference in New Issue