Files
WebChecker/views/admin_websites_view.py
T

461 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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, get_website_assigned_users
data = get_website_by_id(wid)
creds = get_website_credentials(wid)
data["credentials"] = creds
data["assigned_users"] = get_website_assigned_users(wid)
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)
# ── Visibility ────────────────────────────────────────────────────────
ttk.Label(form, text="Visibility").grid(
row=4, column=0, sticky="w", padx=(0, 10), pady=6)
vis_frame = tk.Frame(form, bg=COLOURS["bg"])
vis_frame.grid(row=4, column=1, sticky="w", pady=6)
self.visibility_var = tk.StringVar(value="all")
for val, lbl in [("all", "👥 All Users"), ("assigned", "🔒 Assigned Only")]:
rb = tk.Radiobutton(
vis_frame, text=lbl,
variable=self.visibility_var, value=val,
bg=COLOURS["bg"], fg=COLOURS["text"],
activebackground=COLOURS["bg"],
activeforeground=COLOURS["accent"],
selectcolor=COLOURS["surface2"],
font=FONT, cursor="hand2",
command=self._on_visibility_change,
)
rb.pack(side="left", padx=(0, 16))
# ── Assigned users (shown only when visibility='assigned') ─────────────
self._user_assign_frame = ttk.Frame(self.inner)
self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4))
ttk.Label(self._user_assign_frame, text="Assigned Users",
style="Heading.TLabel").pack(anchor="w", pady=(4, 6))
# Load all active regular users for the picker
from models import get_all_users
try:
all_users = [u for u in get_all_users()
if u["is_active"] and u["role"] == "user"]
except Exception:
all_users = []
self._user_vars = {} # user_id -> BooleanVar
user_grid = tk.Frame(self._user_assign_frame, bg=COLOURS["surface"],
padx=12, pady=8)
user_grid.pack(fill="x")
for i, u in enumerate(all_users):
var = tk.BooleanVar(value=False)
self._user_vars[u["id"]] = var
col, row = i % 3, i // 3
tk.Checkbutton(
user_grid,
text=f"{u['username']} ({u['full_name'] or ''})",
variable=var,
bg=COLOURS["surface"], fg=COLOURS["text"],
activebackground=COLOURS["surface"],
activeforeground=COLOURS["accent"],
selectcolor=COLOURS["surface2"],
font=FONT_SMALL, cursor="hand2",
anchor="w",
).grid(row=row, column=col, sticky="w", padx=8, pady=2)
# Hide initially; shown when visibility='assigned'
self._user_assign_frame.pack_forget()
# ── Credentials section (collapsed by default) ───────────────────────
ttk.Separator(self.inner, orient="horizontal").pack(
fill="x", padx=24, pady=12)
# Toggle header row
cred_toggle = ttk.Frame(self.inner)
cred_toggle.pack(fill="x", padx=24)
ttk.Label(cred_toggle, text="Login Credentials",
style="Heading.TLabel").pack(side="left")
self._cred_expanded = False
self._cred_toggle_btn = ttk.Button(
cred_toggle,
text="🔑 Show Credentials",
style="Ghost.TButton",
command=self._toggle_credentials,
)
self._cred_toggle_btn.pack(side="right")
ttk.Button(cred_toggle, text=" Add Credential",
command=self._add_cred_row_visible).pack(side="right", padx=(0, 6))
# Collapsible body
self._cred_body = ttk.Frame(self.inner)
self.creds_container = ttk.Frame(self._cred_body)
self.creds_container.pack(fill="x")
# _cred_body is NOT packed initially — hidden by default
# ── 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 "")
vis = d.get("visibility") or "all"
self.visibility_var.set(vis)
# Pre-tick assigned users
assigned_ids = {u["id"] for u in d.get("assigned_users", [])}
for uid, var in self._user_vars.items():
var.set(uid in assigned_ids)
self._on_visibility_change()
existing_creds = d.get("credentials", [])
for cred in existing_creds:
self._add_cred_row(cred)
# Auto-expand credentials section if creds already exist
if existing_creds:
self._expand_credentials()
# For new websites: credentials section stays collapsed
def _on_visibility_change(self):
"""Show or hide the user assignment panel based on visibility selection."""
if self.visibility_var.get() == "assigned":
self._user_assign_frame.pack(fill="x", padx=24, pady=(0, 4),
before=self.creds_container)
else:
self._user_assign_frame.pack_forget()
def _expand_credentials(self):
"""Show the credentials body and update the toggle button label."""
self._cred_expanded = True
self._cred_body.pack(fill="x", padx=24, pady=(4, 0))
self._cred_toggle_btn.config(text="🔒 Hide Credentials")
def _toggle_credentials(self):
"""Show or hide the collapsible credentials section."""
if self._cred_expanded:
self._cred_expanded = False
self._cred_body.pack_forget()
self._cred_toggle_btn.config(text="🔑 Show Credentials")
else:
self._expand_credentials()
def _add_cred_row_visible(self):
"""Expand the section (if collapsed) then add an empty credential row."""
if not self._cred_expanded:
self._expand_credentials()
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()
visibility = self.visibility_var.get()
assigned_user_ids = [uid for uid, var in self._user_vars.items() if var.get()]
if not name or not url:
show_error("Name and URL are required.")
return
if visibility == "assigned" and not assigned_user_ids:
show_error("Please assign at least one user, or set visibility to All Users.")
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,
visibility, assigned_user_ids)
logger.info(f"Website id={self.website_data['id']} updated "
f"check_type='{check_type}' visibility='{visibility}'.")
show_info("Website updated successfully.")
else:
from models import create_website
create_website(self.current_user["id"],
name, url, check_type, note, credentials,
visibility, assigned_user_ids)
logger.info(f"New website '{name}' created "
f"check_type='{check_type}' visibility='{visibility}'.")
show_info("Website created successfully.")
self.on_save()
self.destroy()
except Exception as e:
show_error(f"Save failed:\n{e}")