04/21 Fisrt commit

This commit is contained in:
2026-04-21 17:16:37 -04:00
parent f94d86f04b
commit 7548dfc9bf
23 changed files with 6860 additions and 1 deletions
+337
View File
@@ -0,0 +1,337 @@
"""
views/admin_websites_view.py — Admin panel: Website Link Management tab.
Changes: added Check Type (Daily / Weekly) field and treeview column.
"""
import tkinter as tk
from tkinter import ttk
import logging
from utils.ui_helpers import (
COLOURS, FONT, FONT_BOLD, FONT_HEADING, FONT_SMALL,
scrolled_text, show_error, show_info, confirm_delete
)
logger = logging.getLogger("admin_websites_view")
CHECK_TYPE_OPTIONS = ["daily", "weekly"]
CHECK_TYPE_LABELS = {"daily": "📅 Daily", "weekly": "🗓 Weekly"}
class AdminWebsitesView(ttk.Frame):
def __init__(self, parent, current_user: dict):
super().__init__(parent)
self.current_user = current_user
self._build_ui()
self._load_websites()
# ─── Layout ───────────────────────────────────────────────────────────────
def _build_ui(self):
toolbar = ttk.Frame(self)
toolbar.pack(fill="x", pady=(0, 10))
ttk.Label(toolbar, text="Website Link Management",
style="Heading.TLabel").pack(side="left")
ttk.Button(toolbar, text=" Add Website",
command=self._open_add).pack(side="right", padx=(4, 0))
ttk.Button(toolbar, text="✎ Edit",
style="Ghost.TButton",
command=self._open_edit).pack(side="right", padx=(4, 0))
ttk.Button(toolbar, text="✕ Delete",
style="Danger.TButton",
command=self._delete_selected).pack(side="right")
cols = ("ID", "Name", "Type", "URL", "Note", "Created By")
widths = [40, 170, 80, 230, 150, 100]
self.tree = ttk.Treeview(self, columns=cols, show="headings",
selectmode="browse")
for col, w in zip(cols, widths):
self.tree.heading(col, text=col)
self.tree.column(col, width=w,
anchor="center" if w <= 80 else "w")
self.tree.pack(fill="both", expand=True)
self.tree.bind("<Double-1>", lambda _: self._open_edit())
# Tag weekly rows with a subtle colour
self.tree.tag_configure("weekly", foreground=COLOURS["warning"])
vsb = ttk.Scrollbar(self, orient="vertical", command=self.tree.yview)
self.tree.configure(yscrollcommand=vsb.set)
vsb.place(relx=1, rely=0, relheight=1, anchor="ne")
# ─── Data ─────────────────────────────────────────────────────────────────
def _load_websites(self):
from models import get_all_websites
self.tree.delete(*self.tree.get_children())
try:
for w in get_all_websites():
ct = w.get("check_type") or "daily"
tag = "weekly" if ct == "weekly" else ""
self.tree.insert(
"", "end", iid=str(w["id"]), tags=(tag,),
values=(
w["id"],
w["name"],
CHECK_TYPE_LABELS.get(ct, ct),
w["url"],
(w["note"] or "")[:60],
w["creator"] or "",
)
)
except Exception as e:
show_error(f"Failed to load websites:\n{e}")
def _get_selected_id(self):
sel = self.tree.selection()
return int(sel[0]) if sel else None
# ─── Actions ──────────────────────────────────────────────────────────────
def _open_add(self):
WebsiteDialog(self, self.current_user, website_data=None,
on_save=self._load_websites)
def _open_edit(self):
wid = self._get_selected_id()
if not wid:
show_error("Please select a website to edit.")
return
from models import get_website_by_id, get_website_credentials
data = get_website_by_id(wid)
creds = get_website_credentials(wid)
data["credentials"] = creds
WebsiteDialog(self, self.current_user, website_data=data,
on_save=self._load_websites)
def _delete_selected(self):
wid = self._get_selected_id()
if not wid:
show_error("Please select a website to delete.")
return
vals = self.tree.item(wid, "values")
name = vals[1] if vals else str(wid)
if confirm_delete(name):
try:
from models import delete_website
delete_website(self.current_user["id"], wid)
logger.info(f"Website id={wid} deleted by admin.")
show_info(f"Website '{name}' deleted.")
self._load_websites()
except Exception as e:
show_error(f"Delete failed:\n{e}")
# ─── Website Dialog (Add / Edit) ──────────────────────────────────────────────
class WebsiteDialog(tk.Toplevel):
def __init__(self, parent, current_user, website_data, on_save):
super().__init__(parent)
self.current_user = current_user
self.website_data = website_data
self.on_save = on_save
self.is_edit = website_data is not None
self.cred_rows = []
self.title("Edit Website" if self.is_edit else "Add Website")
self.configure(bg=COLOURS["bg"])
self.grab_set()
self._build_ui()
self._centre()
def _centre(self):
self.update_idletasks()
w, h = 600, 680
x = (self.winfo_screenwidth() - w) // 2
y = (self.winfo_screenheight() - h) // 2
self.geometry(f"{w}x{h}+{x}+{y}")
def _build_ui(self):
# Scrollable container
canvas = tk.Canvas(self, bg=COLOURS["bg"], highlightthickness=0)
vsb = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(fill="both", expand=True)
self.inner = ttk.Frame(canvas)
self.inner.bind("<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=self.inner, anchor="nw")
# Scope mousewheel to canvas hover — avoids stale-widget errors on close
def _enter(e):
canvas.bind_all("<MouseWheel>",
lambda ev: _safe_scroll(ev))
def _leave(e):
try:
canvas.unbind_all("<MouseWheel>")
except Exception:
pass
def _safe_scroll(ev):
try:
canvas.yview_scroll(int(-1 * (ev.delta / 120)), "units")
except Exception:
pass
canvas.bind("<Enter>", _enter)
canvas.bind("<Leave>", _leave)
self.protocol("WM_DELETE_WINDOW", lambda: (_leave(None), self.destroy()))
ttk.Label(self.inner,
text="Edit Website" if self.is_edit else "New Website",
style="Heading.TLabel").pack(anchor="w", padx=24, pady=(20, 4))
form = ttk.Frame(self.inner)
form.pack(fill="x", padx=24, pady=4)
form.columnconfigure(1, weight=1)
def text_row(label, r):
ttk.Label(form, text=label).grid(row=r, column=0,
sticky="nw", padx=(0, 10), pady=6)
var = tk.StringVar()
ent = ttk.Entry(form, textvariable=var)
ent.grid(row=r, column=1, sticky="ew", pady=6)
return var
self.name_var = text_row("Name *", 0)
self.url_var = text_row("URL *", 1)
# ── Check Type ────────────────────────────────────────────────────────
ttk.Label(form, text="Check Type").grid(
row=2, column=0, sticky="w", padx=(0, 10), pady=6)
type_frame = tk.Frame(form, bg=COLOURS["bg"])
type_frame.grid(row=2, column=1, sticky="w", pady=6)
self.check_type_var = tk.StringVar(value="daily")
for val, lbl in [("daily", "📅 Daily"), ("weekly", "🗓 Weekly")]:
rb = tk.Radiobutton(
type_frame,
text=lbl,
variable=self.check_type_var,
value=val,
bg=COLOURS["bg"],
fg=COLOURS["text"],
activebackground=COLOURS["bg"],
activeforeground=COLOURS["accent"],
selectcolor=COLOURS["surface2"],
font=FONT,
cursor="hand2",
)
rb.pack(side="left", padx=(0, 16))
# ── Note ──────────────────────────────────────────────────────────────
ttk.Label(form, text="Note").grid(row=3, column=0, sticky="nw",
padx=(0, 10), pady=6)
note_frame, self.note_txt = scrolled_text(form, height=4)
note_frame.grid(row=3, column=1, sticky="ew", pady=6)
# ── Credentials section ───────────────────────────────────────────────
ttk.Separator(self.inner, orient="horizontal").pack(
fill="x", padx=24, pady=12)
cred_header = ttk.Frame(self.inner)
cred_header.pack(fill="x", padx=24)
ttk.Label(cred_header, text="Login Credentials",
style="Heading.TLabel").pack(side="left")
ttk.Button(cred_header, text=" Add Credential",
command=self._add_cred_row).pack(side="right")
self.creds_container = ttk.Frame(self.inner)
self.creds_container.pack(fill="x", padx=24, pady=8)
# ── Buttons ───────────────────────────────────────────────────────────
ttk.Separator(self.inner, orient="horizontal").pack(
fill="x", padx=24, pady=12)
btn_frame = ttk.Frame(self.inner)
btn_frame.pack(fill="x", padx=24, pady=(0, 20))
ttk.Button(btn_frame, text="Save",
command=self._save).pack(side="right", padx=(6, 0))
ttk.Button(btn_frame, text="Cancel", style="Ghost.TButton",
command=self.destroy).pack(side="right")
# Pre-populate if editing
if self.is_edit:
d = self.website_data
self.name_var.set(d.get("name") or "")
self.url_var.set(d.get("url") or "")
self.check_type_var.set(d.get("check_type") or "daily")
self.note_txt.insert("1.0", d.get("note") or "")
for cred in d.get("credentials", []):
self._add_cred_row(cred)
else:
self._add_cred_row()
def _add_cred_row(self, cred=None):
frame = ttk.Frame(self.creds_container, style="Surface.TFrame")
frame.pack(fill="x", pady=4, ipady=4)
label_var = tk.StringVar(value=cred.get("label", "") if cred else "")
user_var = tk.StringVar(value=cred.get("username", "") if cred else "")
pass_var = tk.StringVar(value=cred.get("password", "") if cred else "")
for col_n, (lbl, var, show) in enumerate([
("Label", label_var, None),
("Username", user_var, None),
("Password", pass_var, ""),
]):
tk.Label(frame, text=lbl, bg=COLOURS["surface"],
fg=COLOURS["text_dim"], font=FONT_SMALL).grid(
row=0, column=col_n*2, sticky="w", padx=(8, 2))
ent = ttk.Entry(frame, textvariable=var, show=show or "", width=14)
ent.grid(row=0, column=col_n*2+1, sticky="ew", padx=(0, 8), pady=4)
ttk.Button(frame, text="", style="Danger.TButton", width=3,
command=lambda f=frame: self._remove_cred_row(f)).grid(
row=0, column=6, padx=(0, 8))
frame.columnconfigure(1, weight=1)
frame.columnconfigure(3, weight=1)
frame.columnconfigure(5, weight=1)
self.cred_rows.append((label_var, user_var, pass_var, frame))
def _remove_cred_row(self, frame):
self.cred_rows = [(l, u, p, f) for l, u, p, f in self.cred_rows
if f is not frame]
frame.destroy()
def _save(self):
name = self.name_var.get().strip()
url = self.url_var.get().strip()
check_type = self.check_type_var.get()
note = self.note_txt.get("1.0", "end-1c").strip()
if not name or not url:
show_error("Name and URL are required.")
return
credentials = []
for label_var, user_var, pass_var, _ in self.cred_rows:
u = user_var.get().strip()
if u:
credentials.append({
"label": label_var.get().strip(),
"username": u,
"password": pass_var.get().strip(),
})
try:
if self.is_edit:
from models import update_website
update_website(self.current_user["id"],
self.website_data["id"],
name, url, check_type, note, credentials)
logger.info(f"Website id={self.website_data['id']} updated "
f"check_type='{check_type}'.")
show_info("Website updated successfully.")
else:
from models import create_website
create_website(self.current_user["id"],
name, url, check_type, note, credentials)
logger.info(f"New website '{name}' created "
f"check_type='{check_type}'.")
show_info("Website created successfully.")
self.on_save()
self.destroy()
except Exception as e:
show_error(f"Save failed:\n{e}")