139_spanish-voice-trainer/database.py

290 lines
8.6 KiB
Python
Raw Normal View History

2026-06-24 05:37:22 +00:00
# database.py
import os
2026-08-21 12:24:50 +00:00
import sys
import sqlite3
from PyQt6.QtCore import QSettings
APP_NAME = "SpanishVoiceTrainer"
DEFAULT_DB_FILENAME = "spanish_trainer.db"
def get_default_db_path() -> str:
"""Returns standard macOS Application Support path:
~/Library/Application Support/SpanishVoiceTrainer/spanish_trainer.db
"""
app_support_dir = os.path.expanduser(
f"~/Library/Application Support/{APP_NAME}"
)
os.makedirs(app_support_dir, exist_ok=True)
return os.path.join(app_support_dir, DEFAULT_DB_FILENAME)
def get_db_path() -> str:
"""Retrieves database path cleanly based on execution environment.
- In packaged app mode (sys.frozen): strictly isolates data inside
Application Support unless a valid custom production path is chosen.
Heals stale settings pointing to local dev source paths.
- In dev mode: allows fallback to local project directory.
"""
qs = QSettings(APP_NAME, "Settings")
custom_path = qs.value("database_path", type=str)
# 1. Check custom path saved in QSettings
if custom_path and os.path.exists(custom_path):
# Safeguard for packaged production app:
# Ignore custom paths that point back into local development source folders
if getattr(sys, "frozen", False) and "01_Projects" in custom_path:
default_path = get_default_db_path()
qs.setValue("database_path", default_path) # Repair stale setting
return default_path
return custom_path
# 2. Development mode fallback (uncompiled python runtime)
if not getattr(sys, "frozen", False):
local_dev_db = os.path.join(
os.path.dirname(os.path.abspath(__file__)), DEFAULT_DB_FILENAME
)
if os.path.exists(local_dev_db):
return local_dev_db
# 3. Default production fallback
default_path = get_default_db_path()
qs.setValue("database_path", default_path)
return default_path
def set_db_path(new_path: str):
"""Updates active database path in user preferences."""
qs = QSettings(APP_NAME, "Settings")
qs.setValue("database_path", new_path)
2026-06-24 05:37:22 +00:00
def get_connection():
2026-08-21 12:24:50 +00:00
"""Establishes connection to the active SQLite database."""
db_path = get_db_path()
conn = sqlite3.connect(db_path)
2026-06-24 05:37:22 +00:00
conn.row_factory = sqlite3.Row
return conn
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
def ensure_database_populated():
2026-08-28 00:27:21 +00:00
"""Initializes tables and migrates schemas if running against a new or existing database file."""
2026-06-24 05:37:22 +00:00
conn = get_connection()
cursor = conn.cursor()
try:
2026-08-21 12:24:50 +00:00
# 1. Unified translations table
2026-06-24 05:37:22 +00:00
cursor.execute("""
CREATE TABLE IF NOT EXISTS translations (
translation_id INTEGER PRIMARY KEY AUTOINCREMENT,
es_text TEXT NOT NULL,
en_text TEXT NOT NULL,
source_context TEXT,
tags TEXT,
notes TEXT,
anki_notes TEXT DEFAULT '',
2026-08-28 00:27:21 +00:00
gender TEXT DEFAULT 'Female',
sort_order INTEGER DEFAULT 0
2026-06-24 05:37:22 +00:00
);
""")
2026-08-21 12:24:50 +00:00
2026-08-28 00:27:21 +00:00
# Safely migrate existing databases that do not yet have the sort_order column
cursor.execute("PRAGMA table_info(translations);")
columns = [row["name"] for row in cursor.fetchall()]
if "sort_order" not in columns:
cursor.execute("ALTER TABLE translations ADD COLUMN sort_order INTEGER DEFAULT 0;")
2026-06-24 05:37:22 +00:00
# 2. Key-value configuration table
cursor.execute("""
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
);
""")
conn.commit()
finally:
conn.close()
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
# ==========================================
# SETTINGS CRUD FUNCTIONS
# ==========================================
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
def load_all_settings():
"""Fetches all system configuration properties into a flat Python dictionary."""
conn = get_connection()
cursor = conn.cursor()
settings_dict = {}
try:
cursor.execute("SELECT settings.key, settings.value FROM settings;")
for row in cursor.fetchall():
settings_dict[row["key"]] = row["value"]
finally:
conn.close()
return settings_dict
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
def save_setting_to_db(key, value):
"""Inserts or replaces an application configuration entry."""
conn = get_connection()
cursor = conn.cursor()
try:
2026-08-21 12:24:50 +00:00
cursor.execute(
"""
2026-06-24 05:37:22 +00:00
INSERT OR REPLACE INTO settings (key, value)
VALUES (?, ?);
2026-08-21 12:24:50 +00:00
""",
(key, value),
)
2026-06-24 05:37:22 +00:00
conn.commit()
finally:
conn.close()
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
# ==========================================
# TRANSLATIONS CRUD FUNCTIONS
# ==========================================
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
def get_all_translations_explicit():
2026-08-28 00:27:21 +00:00
"""Retrieves all records using explicit, table-qualified column declarations sorted by sort_order."""
2026-06-24 05:37:22 +00:00
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute("""
SELECT
translations.translation_id,
translations.es_text,
translations.en_text,
translations.source_context,
translations.tags,
translations.notes,
translations.anki_notes,
2026-08-28 00:27:21 +00:00
translations.gender,
translations.sort_order
2026-06-24 05:37:22 +00:00
FROM translations
2026-08-28 00:27:21 +00:00
ORDER BY translations.sort_order ASC, translations.translation_id ASC;
2026-06-24 05:37:22 +00:00
""")
return [dict(row) for row in cursor.fetchall()]
finally:
conn.close()
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
def get_translation_by_id(translation_id):
"""Loads a single unified record row for specific inspection or editing."""
conn = get_connection()
cursor = conn.cursor()
try:
2026-08-21 12:24:50 +00:00
cursor.execute(
"""
2026-06-24 05:37:22 +00:00
SELECT
translations.translation_id,
translations.es_text,
translations.en_text,
translations.source_context,
translations.tags,
translations.notes,
translations.anki_notes,
2026-08-28 00:27:21 +00:00
translations.gender,
translations.sort_order
2026-06-24 05:37:22 +00:00
FROM translations
WHERE translations.translation_id = ?;
2026-08-21 12:24:50 +00:00
""",
(translation_id,),
)
2026-06-24 05:37:22 +00:00
row = cursor.fetchone()
return dict(row) if row else None
finally:
conn.close()
2026-08-21 12:24:50 +00:00
def update_translation_record(
translation_id,
es_text,
en_text,
source_context,
tags,
notes,
gender,
anki_notes,
2026-08-28 00:27:21 +00:00
sort_order=0,
2026-08-21 12:24:50 +00:00
):
"""Saves interface edits directly back into the table using named arguments."""
2026-06-24 05:37:22 +00:00
conn = get_connection()
cursor = conn.cursor()
try:
2026-08-21 12:24:50 +00:00
cursor.execute(
"""
2026-06-24 05:37:22 +00:00
UPDATE translations
SET
es_text = :es,
en_text = :en,
source_context = :ctx,
tags = :tags,
notes = :notes,
gender = :gender,
2026-08-28 00:27:21 +00:00
anki_notes = :anki,
sort_order = :sort_order
WHERE translation_id = :id;
2026-08-21 12:24:50 +00:00
""",
{
"id": translation_id,
"es": es_text,
"en": en_text,
"ctx": source_context,
"tags": tags,
"notes": notes,
"gender": gender,
"anki": anki_notes,
2026-08-28 00:27:21 +00:00
"sort_order": sort_order,
2026-08-21 12:24:50 +00:00
},
)
2026-06-24 05:37:22 +00:00
conn.commit()
finally:
conn.close()
2026-08-21 12:24:50 +00:00
2026-06-24 05:37:22 +00:00
def delete_translation_record(translation_id):
"""Permanently drops a phrase card row from the data index."""
conn = get_connection()
cursor = conn.cursor()
try:
2026-08-21 12:24:50 +00:00
cursor.execute(
"""
2026-06-24 05:37:22 +00:00
DELETE FROM translations
WHERE translations.translation_id = ?;
2026-08-21 12:24:50 +00:00
""",
(translation_id,),
)
2026-06-24 05:37:22 +00:00
conn.commit()
finally:
2026-06-25 12:23:11 +00:00
conn.close()
2026-08-21 12:24:50 +00:00
def insert_translation_record(
2026-08-28 00:27:21 +00:00
es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order=0
2026-08-21 12:24:50 +00:00
):
"""Inserts a new record using named arguments."""
conn = get_connection()
2026-06-25 12:23:11 +00:00
cursor = conn.cursor()
try:
2026-08-21 12:24:50 +00:00
cursor.execute(
"""
2026-08-28 00:27:21 +00:00
INSERT INTO translations (es_text, en_text, source_context, tags, notes, gender, anki_notes, sort_order)
VALUES (:es, :en, :ctx, :tags, :notes, :gender, :anki, :sort_order);
2026-08-21 12:24:50 +00:00
""",
{
"en": en_text,
"es": es_text,
"ctx": source_context,
"tags": tags,
"notes": notes,
"anki": anki_notes,
"gender": gender,
2026-08-28 00:27:21 +00:00
"sort_order": sort_order,
2026-08-21 12:24:50 +00:00
},
)
2026-06-25 12:23:11 +00:00
conn.commit()
finally:
2026-08-21 12:24:50 +00:00
conn.close()