04/21 Fisrt commit
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
views/settings_view.py — Database connection settings dialog.
|
||||
|
||||
Shown automatically on first run (no config.ini) and accessible
|
||||
via the admin sidebar Settings nav item at any time.
|
||||
|
||||
Writes connection details to config.ini via config.save_config().
|
||||
Does NOT store the password in plaintext beyond what config.ini holds
|
||||
(which is acceptable for a locally-run desktop tool; operators should
|
||||
restrict file-system access to config.ini in production).
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from utils.ui_helpers import (
|
||||
COLOURS, FONT, FONT_BOLD, FONT_HEADING, FONT_SMALL,
|
||||
show_error, show_info,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("settings_view")
|
||||
|
||||
|
||||
class SettingsView(tk.Toplevel):
|
||||
"""
|
||||
Modal dialog for DB connection settings.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
master : tk.Tk or tk.Toplevel parent
|
||||
on_save_callback: called (no args) after settings are saved and verified
|
||||
first_run : True => locked modal, no Cancel, title says "Setup"
|
||||
False => dismissible, title says "Settings"
|
||||
"""
|
||||
|
||||
def __init__(self, master, on_save_callback, first_run: bool = False):
|
||||
super().__init__(master)
|
||||
self.on_save_callback = on_save_callback
|
||||
self.first_run = first_run
|
||||
|
||||
self.title("Database Setup" if first_run else "Connection Settings")
|
||||
self.configure(bg=COLOURS["bg"])
|
||||
self.resizable(False, False)
|
||||
self.grab_set()
|
||||
|
||||
if first_run:
|
||||
self.protocol("WM_DELETE_WINDOW", lambda: None) # prevent close
|
||||
else:
|
||||
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
||||
|
||||
self._build_ui()
|
||||
self._centre()
|
||||
self._load_existing()
|
||||
|
||||
# ─── Layout ───────────────────────────────────────────────────────────────
|
||||
|
||||
def _centre(self):
|
||||
self.update_idletasks()
|
||||
w, h = 480, 480
|
||||
x = (self.winfo_screenwidth() - w) // 2
|
||||
y = (self.winfo_screenheight() - h) // 2
|
||||
self.geometry(f"{w}x{h}+{x}+{y}")
|
||||
|
||||
def _build_ui(self):
|
||||
# ── Header ────────────────────────────────────────────────────────────
|
||||
hdr = tk.Frame(self, bg=COLOURS["accent"], pady=16)
|
||||
hdr.pack(fill="x")
|
||||
tk.Label(hdr,
|
||||
text="Database Setup" if self.first_run else "Connection Settings",
|
||||
font=FONT_BOLD, bg=COLOURS["accent"],
|
||||
fg=COLOURS["white"]).pack()
|
||||
if self.first_run:
|
||||
tk.Label(hdr,
|
||||
text="Enter your MySQL connection details to get started.",
|
||||
font=FONT_SMALL, bg=COLOURS["accent"],
|
||||
fg=COLOURS["white"]).pack(pady=(2, 0))
|
||||
|
||||
# ── Form ──────────────────────────────────────────────────────────────
|
||||
form = tk.Frame(self, bg=COLOURS["bg"], padx=36, pady=20)
|
||||
form.pack(fill="both", expand=True)
|
||||
form.columnconfigure(1, weight=1)
|
||||
|
||||
def field(label, row, show=None, width=30):
|
||||
tk.Label(form, text=label, bg=COLOURS["bg"],
|
||||
fg=COLOURS["text_dim"], font=FONT_SMALL,
|
||||
anchor="w").grid(row=row, column=0,
|
||||
sticky="w", padx=(0, 12), pady=6)
|
||||
var = tk.StringVar()
|
||||
ent = tk.Entry(form, textvariable=var, width=width,
|
||||
bg=COLOURS["surface2"], fg=COLOURS["text"],
|
||||
insertbackground=COLOURS["text"],
|
||||
relief="flat", font=FONT,
|
||||
show=show or "")
|
||||
ent.grid(row=row, column=1, sticky="ew", ipady=6, pady=6)
|
||||
return var, ent
|
||||
|
||||
self.host_var, self._host_ent = field("Host *", 0)
|
||||
self.port_var, _ = field("Port *", 1, width=8)
|
||||
self.database_var, _ = field("Database Name *", 2)
|
||||
self.user_var, _ = field("Username *", 3)
|
||||
self.password_var, _ = field("Password", 4, show="•")
|
||||
|
||||
self.port_var.set("3306") # sensible default
|
||||
|
||||
# ── Status line ───────────────────────────────────────────────────────
|
||||
self._status_var = tk.StringVar(value="")
|
||||
self._status_lbl = tk.Label(
|
||||
form, textvariable=self._status_var,
|
||||
bg=COLOURS["bg"], fg=COLOURS["text_dim"],
|
||||
font=FONT_SMALL, anchor="w", wraplength=380
|
||||
)
|
||||
self._status_lbl.grid(row=5, column=0, columnspan=2,
|
||||
sticky="ew", pady=(4, 0))
|
||||
|
||||
# ── Hint ──────────────────────────────────────────────────────────────
|
||||
hint = (
|
||||
"Settings are saved to config.ini in the application folder.\n"
|
||||
"Ensure the database user has CREATE, INSERT, UPDATE, DELETE privileges."
|
||||
)
|
||||
tk.Label(form, text=hint, bg=COLOURS["bg"],
|
||||
fg=COLOURS["text_dim"], font=FONT_SMALL,
|
||||
justify="left", wraplength=380).grid(
|
||||
row=6, column=0, columnspan=2, sticky="w", pady=(8, 0))
|
||||
|
||||
# ── Buttons ───────────────────────────────────────────────────────────
|
||||
btn_row = tk.Frame(self, bg=COLOURS["bg"], padx=36, pady=16)
|
||||
btn_row.pack(fill="x")
|
||||
|
||||
self._save_btn = tk.Button(
|
||||
btn_row, text="Save & Connect",
|
||||
command=self._save,
|
||||
bg=COLOURS["accent"], fg=COLOURS["white"],
|
||||
activebackground=COLOURS["accent_hover"],
|
||||
activeforeground=COLOURS["white"],
|
||||
relief="flat", font=FONT_BOLD,
|
||||
cursor="hand2", padx=14, pady=8,
|
||||
)
|
||||
self._save_btn.pack(side="right", padx=(8, 0))
|
||||
|
||||
self._test_btn = tk.Button(
|
||||
btn_row, text="Test Connection",
|
||||
command=self._test_connection,
|
||||
bg=COLOURS["surface2"], fg=COLOURS["text"],
|
||||
activebackground=COLOURS["surface2"],
|
||||
activeforeground=COLOURS["accent"],
|
||||
relief="flat", font=FONT_SMALL,
|
||||
cursor="hand2", padx=10, pady=8,
|
||||
)
|
||||
self._test_btn.pack(side="right")
|
||||
|
||||
if not self.first_run:
|
||||
tk.Button(
|
||||
btn_row, text="Cancel",
|
||||
command=self.destroy,
|
||||
bg=COLOURS["surface"], fg=COLOURS["text_dim"],
|
||||
activebackground=COLOURS["surface2"],
|
||||
relief="flat", font=FONT_SMALL,
|
||||
cursor="hand2", padx=10, pady=8,
|
||||
).pack(side="left")
|
||||
|
||||
# ─── Prefill from existing config ─────────────────────────────────────────
|
||||
|
||||
def _load_existing(self):
|
||||
from config import load_config
|
||||
ini = load_config()
|
||||
if ini:
|
||||
self.host_var.set(ini.get("host", ""))
|
||||
self.port_var.set(str(ini.get("port", 3306)))
|
||||
self.database_var.set(ini.get("database", ""))
|
||||
self.user_var.set(ini.get("user", ""))
|
||||
self.password_var.set(ini.get("password", ""))
|
||||
|
||||
# ─── Validation ───────────────────────────────────────────────────────────
|
||||
|
||||
def _get_fields(self):
|
||||
"""Return (host, port, database, user, password) or None on error."""
|
||||
host = self.host_var.get().strip()
|
||||
port_str = self.port_var.get().strip()
|
||||
database = self.database_var.get().strip()
|
||||
user = self.user_var.get().strip()
|
||||
password = self.password_var.get()
|
||||
|
||||
if not host or not database or not user:
|
||||
self._set_status("Host, Database Name, and Username are required.", "danger")
|
||||
return None
|
||||
try:
|
||||
port = int(port_str)
|
||||
if not (1 <= port <= 65535):
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self._set_status("Port must be a number between 1 and 65535.", "danger")
|
||||
return None
|
||||
|
||||
return host, port, database, user, password
|
||||
|
||||
def _set_status(self, message: str, level: str = "dim"):
|
||||
colour_map = {
|
||||
"danger": COLOURS["danger"],
|
||||
"success": COLOURS["success"],
|
||||
"warning": COLOURS["warning"],
|
||||
"dim": COLOURS["text_dim"],
|
||||
}
|
||||
self._status_lbl.config(fg=colour_map.get(level, COLOURS["text_dim"]))
|
||||
self._status_var.set(message)
|
||||
|
||||
# ─── Test Connection ──────────────────────────────────────────────────────
|
||||
|
||||
def _test_connection(self):
|
||||
fields = self._get_fields()
|
||||
if not fields:
|
||||
return
|
||||
host, port, database, user, password = fields
|
||||
|
||||
self._set_status("Testing connection...", "dim")
|
||||
self._test_btn.config(state="disabled")
|
||||
self._save_btn.config(state="disabled")
|
||||
|
||||
# Run in a background thread so the UI stays responsive
|
||||
def _run():
|
||||
try:
|
||||
import mysql.connector
|
||||
conn = mysql.connector.connect(
|
||||
host=host, port=port, database=database,
|
||||
user=user, password=password,
|
||||
connection_timeout=8,
|
||||
)
|
||||
conn.close()
|
||||
self.after(0, lambda: self._set_status(
|
||||
"Connection successful!", "success"))
|
||||
logger.info(f"Test connection to {host}:{port}/{database} succeeded.")
|
||||
except Exception as e:
|
||||
self.after(0, lambda err=e: self._set_status(
|
||||
f"Connection failed: {err}", "danger"))
|
||||
logger.warning(f"Test connection failed: {e}")
|
||||
finally:
|
||||
self.after(0, lambda: self._test_btn.config(state="normal"))
|
||||
self.after(0, lambda: self._save_btn.config(state="normal"))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
# ─── Save ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _save(self):
|
||||
fields = self._get_fields()
|
||||
if not fields:
|
||||
return
|
||||
host, port, database, user, password = fields
|
||||
|
||||
self._set_status("Saving and connecting...", "dim")
|
||||
self._save_btn.config(state="disabled")
|
||||
self._test_btn.config(state="disabled")
|
||||
|
||||
def _run():
|
||||
try:
|
||||
# Verify the connection before persisting
|
||||
import mysql.connector
|
||||
conn = mysql.connector.connect(
|
||||
host=host, port=port, database=database,
|
||||
user=user, password=password,
|
||||
connection_timeout=8,
|
||||
)
|
||||
conn.close()
|
||||
|
||||
from config import save_config
|
||||
save_config(host, port, database, user, password)
|
||||
logger.info(f"Settings saved: {user}@{host}:{port}/{database}")
|
||||
|
||||
self.after(0, self._on_save_success)
|
||||
except Exception as e:
|
||||
self.after(0, lambda err=e: self._set_status(
|
||||
f"Could not connect: {err}", "danger"))
|
||||
logger.error(f"Settings save failed: {e}")
|
||||
finally:
|
||||
self.after(0, lambda: self._save_btn.config(state="normal"))
|
||||
self.after(0, lambda: self._test_btn.config(state="normal"))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
|
||||
def _on_save_success(self):
|
||||
self._set_status("Saved successfully.", "success")
|
||||
self.after(400, self._finish)
|
||||
|
||||
def _finish(self):
|
||||
self.destroy()
|
||||
self.on_save_callback()
|
||||
Reference in New Issue
Block a user