04/21 Fisrt commit
This commit is contained in:
@@ -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}")
|
||||
Reference in New Issue
Block a user