feat(trainer): add GitHub activity heatmap, 4-option quiz mode, active typing, and duplicate checks
This commit is contained in:
1 parent
d31c77fdd0
commit
2294121777
16 files changed
+4318
-685
No files matched your search
+77
-11
@@ -3,17 +3,16 @@ import contextlib
|
||||
from typing import Generator
|
||||
from app import config
|
||||
|
||||
|
||||
def get_db_connection() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(
|
||||
config.DB_PATH,
|
||||
timeout=15.0
|
||||
)
|
||||
conn = sqlite3.connect(config.DB_PATH, timeout=15.0)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL;")
|
||||
conn.execute("PRAGMA synchronous=NORMAL;")
|
||||
conn.execute("PRAGMA foreign_keys=ON;")
|
||||
return conn
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def get_db() -> Generator[sqlite3.Connection, None, None]:
|
||||
conn = get_db_connection()
|
||||
@@ -26,6 +25,46 @@ def get_db() -> Generator[sqlite3.Connection, None, None]:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _migrate(conn: sqlite3.Connection):
|
||||
"""Safe schema migrations: add new columns/tables without touching existing data."""
|
||||
# --- vocabulary_cards new columns ---
|
||||
existing_cols = {
|
||||
row[1] for row in conn.execute("PRAGMA table_info(vocabulary_cards)").fetchall()
|
||||
}
|
||||
card_migrations = [
|
||||
("collection", "TEXT NOT NULL DEFAULT 'General'"),
|
||||
("srs_reps", "INTEGER DEFAULT 0"),
|
||||
("srs_interval", "INTEGER DEFAULT 0"),
|
||||
("srs_ease", "REAL DEFAULT 2.5"),
|
||||
("srs_next_review", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"),
|
||||
("srs_last_review", "TIMESTAMP"),
|
||||
("srs_state", "TEXT DEFAULT 'new'"),
|
||||
("error_count", "INTEGER DEFAULT 0"),
|
||||
]
|
||||
for col, definition in card_migrations:
|
||||
if col not in existing_cols:
|
||||
conn.execute(f"ALTER TABLE vocabulary_cards ADD COLUMN {col} {definition}")
|
||||
|
||||
# Ensure srs_logs table exists
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS srs_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
card_id INTEGER NOT NULL,
|
||||
rating INTEGER NOT NULL,
|
||||
interval INTEGER NOT NULL,
|
||||
ease REAL NOT NULL,
|
||||
reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (card_id) REFERENCES vocabulary_cards(id) ON DELETE CASCADE
|
||||
);
|
||||
""")
|
||||
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_srs_user ON srs_logs(user_id);")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_srs_reviewed ON srs_logs(user_id, reviewed_at);")
|
||||
|
||||
|
||||
def init_db():
|
||||
config.DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with get_db() as conn:
|
||||
@@ -53,13 +92,21 @@ def init_db():
|
||||
user_id INTEGER NOT NULL,
|
||||
term TEXT NOT NULL,
|
||||
translation TEXT NOT NULL,
|
||||
context_sentence TEXT,
|
||||
phonetic TEXT,
|
||||
audio_url TEXT,
|
||||
part_of_speech TEXT,
|
||||
definition_en TEXT,
|
||||
tags TEXT,
|
||||
notes TEXT,
|
||||
context_sentence TEXT DEFAULT '',
|
||||
phonetic TEXT DEFAULT '',
|
||||
audio_url TEXT DEFAULT '',
|
||||
part_of_speech TEXT DEFAULT '',
|
||||
definition_en TEXT DEFAULT '',
|
||||
tags TEXT DEFAULT 'general',
|
||||
notes TEXT DEFAULT '',
|
||||
collection TEXT NOT NULL DEFAULT 'General',
|
||||
srs_reps INTEGER DEFAULT 0,
|
||||
srs_interval INTEGER DEFAULT 0,
|
||||
srs_ease REAL DEFAULT 2.5,
|
||||
srs_next_review TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
srs_last_review TIMESTAMP,
|
||||
srs_state TEXT DEFAULT 'new',
|
||||
error_count INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
@@ -68,6 +115,23 @@ def init_db():
|
||||
CREATE INDEX IF NOT EXISTS idx_vocab_user ON vocabulary_cards(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_vocab_term ON vocabulary_cards(term);
|
||||
CREATE INDEX IF NOT EXISTS idx_vocab_created ON vocabulary_cards(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_vocab_collection ON vocabulary_cards(user_id, collection);
|
||||
CREATE INDEX IF NOT EXISTS idx_vocab_srs_queue ON vocabulary_cards(user_id, srs_next_review, srs_state);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS srs_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
card_id INTEGER NOT NULL,
|
||||
rating INTEGER NOT NULL,
|
||||
interval INTEGER NOT NULL,
|
||||
ease REAL NOT NULL,
|
||||
reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (card_id) REFERENCES vocabulary_cards(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_srs_user ON srs_logs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_srs_reviewed ON srs_logs(user_id, reviewed_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dictionary_cache (
|
||||
query TEXT PRIMARY KEY COLLATE NOCASE,
|
||||
@@ -75,3 +139,5 @@ def init_db():
|
||||
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
""")
|
||||
# Apply migrations for pre-existing databases
|
||||
_migrate(conn)
|
||||
Reference in new issue
Block a user